├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── LICENSE ├── README.md ├── app.json ├── babel.config.js ├── index.js ├── macos ├── .gitignore ├── Podfile ├── Podfile.lock ├── cidemon-iOS │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m ├── cidemon-macOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-1024.png │ │ │ ├── Icon-128.png │ │ │ ├── Icon-16.png │ │ │ ├── Icon-16@2x.png │ │ │ ├── Icon-256.png │ │ │ ├── Icon-257.png │ │ │ ├── Icon-32.png │ │ │ ├── Icon-32@2x.png │ │ │ ├── Icon-512.png │ │ │ └── Icon-513.png │ │ ├── Contents.json │ │ ├── copy.imageset │ │ │ ├── Contents.json │ │ │ └── copy.pdf │ │ ├── failed.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 106.png │ │ │ ├── Group 106@2x.png │ │ │ ├── Group 106@3x.png │ │ │ ├── Group 98.png │ │ │ ├── Group 98@2x.png │ │ │ └── Group 98@3x.png │ │ ├── failed_passed.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 101.png │ │ │ ├── Group 101@2x.png │ │ │ ├── Group 101@3x.png │ │ │ ├── Group 109.png │ │ │ ├── Group 109@2x.png │ │ │ └── Group 109@3x.png │ │ ├── initial.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 107.png │ │ │ ├── Group 107@2x.png │ │ │ ├── Group 107@3x.png │ │ │ ├── Group 99.png │ │ │ ├── Group 99@2x.png │ │ │ └── Group 99@3x.png │ │ ├── passed.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 104.png │ │ │ ├── Group 104@2x.png │ │ │ ├── Group 104@3x.png │ │ │ ├── Group 96.png │ │ │ ├── Group 96@2x.png │ │ │ └── Group 96@3x.png │ │ ├── pending.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 105.png │ │ │ ├── Group 105@2x.png │ │ │ ├── Group 105@3x.png │ │ │ ├── Group 97.png │ │ │ ├── Group 97@2x.png │ │ │ └── Group 97@3x.png │ │ ├── pending_failed.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 103.png │ │ │ ├── Group 103@2x.png │ │ │ ├── Group 103@3x.png │ │ │ ├── Group 95.png │ │ │ ├── Group 95@2x.png │ │ │ └── Group 95@3x.png │ │ ├── pending_failed_passed.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 102.png │ │ │ ├── Group 102@2x-1.png │ │ │ ├── Group 102@2x.png │ │ │ ├── Group 110.png │ │ │ ├── Group 110@3x-1.png │ │ │ └── Group 110@3x.png │ │ ├── pending_passed.imageset │ │ │ ├── Contents.json │ │ │ ├── Group 100.png │ │ │ ├── Group 100@2x.png │ │ │ ├── Group 100@3x.png │ │ │ ├── Group 108.png │ │ │ ├── Group 108@2x.png │ │ │ └── Group 108@3x.png │ │ ├── simple_failed.imageset │ │ │ ├── Contents.json │ │ │ ├── Union.png │ │ │ ├── Union@2x.png │ │ │ └── Union@3x.png │ │ ├── simple_initial.imageset │ │ │ ├── Contents.json │ │ │ ├── Union-3.png │ │ │ ├── Union-4.png │ │ │ ├── Union@2x-3.png │ │ │ ├── Union@2x-4.png │ │ │ ├── Union@3x-3.png │ │ │ └── Union@3x-4.png │ │ ├── simple_passed.imageset │ │ │ ├── Contents.json │ │ │ ├── Union-2.png │ │ │ ├── Union@2x-2.png │ │ │ └── Union@3x-2.png │ │ └── simple_pending.imageset │ │ │ ├── Contents.json │ │ │ ├── Union-1.png │ │ │ ├── Union@2x-1.png │ │ │ └── Union@3x-1.png │ ├── Base.lproj │ │ └── Main.storyboard │ ├── CIDemonNative.m │ ├── CIDemonNative.swift │ ├── Info.plist │ ├── cidemon-macOS-Bridging-Header.h │ ├── cidemon.entitlements │ └── lib │ │ ├── MyNSWindowDelegate.swift │ │ ├── ReactNativeBridge.swift │ │ └── ReactViewController.swift ├── cidemon.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── cidemon-iOS.xcscheme │ │ └── cidemon-macOS.xcscheme └── cidemon.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── metro.config.js ├── package.json ├── scripts └── bumpVersion.sh ├── src ├── App.tsx ├── Assets.ts ├── Root.store.ts ├── Route.tsx ├── Theme.ts ├── assets │ ├── icons │ │ ├── appcenter.png │ │ ├── bitrise.png │ │ ├── circleci.png │ │ ├── github.png │ │ ├── github_bar.png │ │ ├── github_bar@2x.png │ │ ├── github_bar@3x.png │ │ ├── gitlab.png │ │ ├── logo.png │ │ └── travisci.png │ └── image │ │ └── profile.jpeg ├── component │ ├── Divider.component.tsx │ ├── EmptyNodes.component.tsx │ ├── NodeRow.component.tsx │ ├── Row.component.tsx │ ├── Spacer.component.tsx │ ├── TempoButton.component.tsx │ └── index.ts ├── container │ ├── AddToken.container.tsx │ ├── GeneralConfig.container.tsx │ ├── GithubActionsConfig.container.tsx │ ├── IgnoreConfig.container.tsx │ ├── NodeList.container.tsx │ ├── Toast.container.tsx │ └── index.ts ├── lib │ ├── CIDemonNative.ts │ ├── DtoMapper.test.ts │ ├── DtoMappings.ts │ ├── __mocks__ │ │ └── CIDemonNative.ts │ ├── constants.ts │ ├── generalUtils.ts │ ├── hooks.ts │ └── index.ts ├── model │ ├── dto │ │ ├── AppcenterBranch.dto.ts │ │ ├── AppcenterRepo.dto.ts │ │ ├── Bitrise.dto.ts │ │ ├── CircleciRepo.dto.ts │ │ ├── Github.dto.ts │ │ ├── Gitlab.dto.ts │ │ ├── TravisBranches.dto.ts │ │ └── TravisRepos.dto.ts │ └── index.ts ├── store │ ├── Api.store.ts │ ├── Node.store.test.ts │ ├── Node.store.ts │ ├── Ping.store.ts │ ├── UI.store.ts │ ├── __mocks__ │ │ └── Api.store.ts │ └── index.ts ├── styles.json ├── tailwind.ts └── typings.d.ts ├── styles.json ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI Demon Tests 2 | on: push 3 | 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Cancel Previous Runs 10 | uses: styfle/cancel-workflow-action@0.6.0 11 | with: 12 | access_token: ${{ github.token }} 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 12.x 15 | uses: actions/setup-node@v2.1.2 16 | with: 17 | node-version: 15.x 18 | - name: Install Dependencies 19 | run: yarn 20 | - name: Run tests 21 | run: yarn test 22 | # - name: Run tests 23 | # uses: ospfranco/gh-jester@v1.0.13 24 | # with: 25 | # post-comment: true 26 | # env: 27 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | # GITHUB_CONTEXT: ${{ toJson(github) }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Oscar Franco 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

CI Demon

2 | 3 |

A macOS menu bar application to monitor your CI jobs

4 | 5 | ![cidemon](https://user-images.githubusercontent.com/1634213/129091153-df219f7c-6094-497a-a4eb-17ecf7a0a4b5.png) 6 | 7 |
8 |
 9 |     Get it on the mac app store
10 |     Or build it yourself!
11 |   
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | MacOS menu bar app that aggregates information and notifies you when your CI builds or deployments are broken (or restored), it shows you all the relevant information in a single native app, without having to wait for entire web apps to load. 24 | 25 | Developed with react-native for macOS, if you know typescript you can easily contribute to it, if you want to add support for more providers, it is fairly straightforward, your CI needs to provide an API and you need only to write a few typescript functions. 26 | 27 | ## Supported providers 28 | 29 | CI Demon currently supports the following CI providers: 30 | 31 | - Github Checks 32 | - CircleCI 33 | - TravisCI 34 | - Gitlab SaaS & self-managed 35 | - AppCenter 36 | - Bitrise 37 | 38 | ## Health Checks 39 | 40 | CI Demon can also create http ping checks for you to make sure your deployment is still running, feature is really simple at the moment, send request -> check for OK response. 41 | 42 | ## All features 43 | 44 | - Desktop notifications on failures/restorations 45 | - Everything is stored encrypted in the macOS keychain 46 | - Absolutely **NO** tracking 47 | - Not a SaaS, it's a native macOS app 48 | - Filter branches/builds by Regex 49 | - Trigger rebuilds for builds 50 | - Track Github PRs and/or branches 51 | - Natively share a job to your team mates 52 | - Filter Gitlab projects by visibility settings 53 | ## License 54 | 55 | MIT License 56 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cidemon", 3 | "displayName": "cidemon" 4 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [ 4 | [ 5 | `module-resolver`, 6 | { 7 | root: [`./src`], 8 | extensions: [`.js`, `.ts`, `.tsx`, `.ios.js`, `.android.js`], 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {AppRegistry} from 'react-native'; 2 | import {App} from './src/App'; 3 | import {name as appName} from './app.json'; 4 | // import {NativeModules} from 'react-native'; 5 | 6 | // NativeModules.DevSettings.setIsSecondaryClickToShowDevMenuEnabled(true); 7 | 8 | AppRegistry.registerComponent(appName, () => App); 9 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | Pods/ 3 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native-macos/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | target 'cidemon-macOS' do 5 | platform :macos, '10.14' 6 | use_native_modules! 7 | use_react_native!( 8 | :path => '../node_modules/react-native-macos', 9 | 10 | # To use Hermes, install the `hermes-engine-darwin` npm package, e.g.: 11 | # $ yarn add 'hermes-engine-darwin@~0.5.3' 12 | # 13 | # Then enable this option: 14 | # :hermes_enabled => true 15 | ) 16 | 17 | # Pods specifically for macOS target 18 | pod 'KeychainAccess' 19 | end 20 | 21 | target 'cidemon-iOS' do 22 | platform :ios, '10' 23 | use_native_modules! 24 | use_react_native!(:path => '../node_modules/react-native-macos') 25 | 26 | # Enables Flipper. 27 | # 28 | # Note that if you have use_frameworks! enabled, Flipper will not work and 29 | # you should disable these next few lines. 30 | # use_flipper! 31 | # post_install do |installer| 32 | # flipper_post_install(installer) 33 | # end 34 | 35 | # Pods specifically for iOS target 36 | end 37 | -------------------------------------------------------------------------------- /macos/cidemon-iOS/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /macos/cidemon-iOS/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | @implementation AppDelegate 8 | 9 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 10 | { 11 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 12 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 13 | moduleName:@"cidemon" 14 | initialProperties:nil]; 15 | 16 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 17 | 18 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 19 | UIViewController *rootViewController = [UIViewController new]; 20 | rootViewController.view = rootView; 21 | self.window.rootViewController = rootViewController; 22 | [self.window makeKeyAndVisible]; 23 | return YES; 24 | } 25 | 26 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 27 | { 28 | #if DEBUG 29 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 30 | #else 31 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 32 | #endif 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /macos/cidemon-iOS/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /macos/cidemon-iOS/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /macos/cidemon-iOS/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /macos/cidemon-iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | $(PRODUCT_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /macos/cidemon-iOS/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | @NSApplicationMain 5 | class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate { 6 | var myWindowDelegate: MyNSWindowDelegate! 7 | var popover: NSWindow! 8 | var window: NSWindow! 9 | var statusBarItem: NSStatusItem! 10 | var reactNativeBridge: ReactNativeBridge! 11 | var controller: ReactViewController! 12 | 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | reactNativeBridge = ReactNativeBridge() 15 | controller = ReactViewController(moduleName: "cidemon", bridge: reactNativeBridge.bridge) 16 | 17 | var screenHeight = CGFloat(800) 18 | if(NSScreen.main != nil) { 19 | screenHeight = NSScreen.main!.frame.height 20 | } 21 | 22 | let windowHeight = Int(min(screenHeight / 1.5, 700)) 23 | let windowWidth = Int(min(screenHeight, 600)) 24 | 25 | popover = NSWindow( 26 | contentRect: NSRect(x: 0, y: 0, width: 1, height: 1), 27 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 28 | backing: .buffered, defer: false) 29 | 30 | popover.titlebarAppearsTransparent = true 31 | popover.titleVisibility = .hidden 32 | popover.isMovableByWindowBackground = false 33 | popover.isReleasedWhenClosed = false 34 | popover.collectionBehavior = [.transient, .ignoresCycle] 35 | popover.standardWindowButton(.closeButton)?.isHidden = true 36 | popover.standardWindowButton(.zoomButton)?.isHidden = true 37 | popover.standardWindowButton(.miniaturizeButton)?.isHidden = true 38 | popover.contentViewController = controller 39 | 40 | self.myWindowDelegate = MyNSWindowDelegate(resignHandler: { 41 | self.hidePopover() 42 | }) 43 | popover.delegate = myWindowDelegate 44 | 45 | statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength)) 46 | 47 | if let button = self.statusBarItem.button { 48 | button.imagePosition = NSControl.ImagePosition.imageLeft 49 | button.image = NSImage(named: "initial") 50 | button.action = #selector(togglePopover(_:)) 51 | } 52 | 53 | NSUserNotificationCenter.default.delegate = self 54 | 55 | // #if DEBUG 56 | // window = NSWindow( 57 | // contentRect: NSRect(x: 0, y: 0, width: 1, height: 1), 58 | // styleMask: [.titled, .closable, .miniaturizable, .resizable], 59 | // backing: .buffered, 60 | // defer: false) 61 | 62 | // window.contentViewController = controller 63 | // window.center() 64 | // window.setFrameAutosaveName("CI Demon") 65 | // window.isReleasedWhenClosed = false 66 | // let screen: NSScreen = NSScreen.main! 67 | // let midScreenX = 600 68 | // let posScreenY = screen.frame.height 69 | // let origin = CGPoint(x: Int(midScreenX), y: Int(posScreenY)) 70 | // let size = CGSize(width: 600, height: 700) 71 | // let frame = NSRect(origin: origin, size: size) 72 | // window.setFrame(frame, display: true) 73 | // window.makeKeyAndOrderFront(self) 74 | // #endif 75 | 76 | } 77 | 78 | func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { 79 | let url = URL(string: notification.userInfo!["url"] as! String)! 80 | NSWorkspace.shared.open(url) 81 | } 82 | 83 | @objc func togglePopover(_ sender: AnyObject?) { 84 | if self.popover.isVisible && self.popover.isKeyWindow { 85 | self.popover.close() 86 | 87 | } else { 88 | 89 | self.popover.makeKeyAndOrderFront(self) 90 | self.popover.center() 91 | if !NSApp.isActive { 92 | NSApp.activate(ignoringOtherApps: true) 93 | } 94 | let screen: NSScreen = NSScreen.main! 95 | let midScreenX = self.statusBarItem.button!.window!.frame.origin.x - 300 96 | let posScreenY = screen.frame.height 97 | let origin = CGPoint(x: Int(midScreenX), y: Int(posScreenY)) 98 | let size = CGSize(width: 600, height: 700) 99 | let frame = NSRect(origin: origin, size: size) 100 | self.popover.setFrame(frame, display: true) 101 | } 102 | } 103 | 104 | @objc func hidePopover() { 105 | if self.popover.isVisible { 106 | self.popover.close() 107 | } 108 | } 109 | 110 | // @objc func showPopover(_ sender: AnyObject?) { 111 | // if !(self.mainWindow.isVisible && self.mainWindow.isKeyWindow) { 112 | // self.hotKey.isPaused = false 113 | // self.mainWindow.makeKeyAndOrderFront(self) 114 | // self.mainWindow.center() 115 | // if !NSApp.isActive { 116 | // NSApp.activate(ignoringOtherApps: true) 117 | // } 118 | // } 119 | // } 120 | 121 | func setStatusText(failed: NSInteger, running: NSInteger, passed: NSInteger, useSimpleIcon: Bool) { 122 | if(useSimpleIcon) { 123 | if let button = self.statusBarItem.button { 124 | if(running > 0) { 125 | button.image = NSImage(named: "simple_pending") 126 | } else if(failed > 0) { 127 | button.image = NSImage(named: "simple_failed") 128 | } else if(running > 0) { 129 | button.image = NSImage(named: "simple_passed") 130 | } else { 131 | button.image = NSImage(named: "simple_initial") 132 | } 133 | } 134 | return 135 | } 136 | 137 | var statuses: [String] = [] 138 | if(running > 0) { 139 | statuses.append("pending") 140 | } 141 | if(failed > 0) { 142 | statuses.append("failed") 143 | } 144 | if(passed > 0) { 145 | statuses.append("passed") 146 | } 147 | 148 | if let button = self.statusBarItem.button { 149 | if(statuses.count > 0) { 150 | button.image = NSImage(named: statuses.joined(separator: "_")) 151 | } else { 152 | button.image = NSImage(named: "initial") 153 | } 154 | } 155 | } 156 | 157 | func closeApp() { 158 | NSApp.terminate(nil) 159 | } 160 | 161 | func getWindowObject() -> NSView { 162 | return controller.view 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-257.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-513.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-16.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-257.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-257.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-512.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-513.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/AppIcon.appiconset/Icon-513.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/copy.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "copy.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/copy.imageset/copy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/copy.imageset/copy.pdf -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 98.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 106.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 98@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 106@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 98@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 106@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 106.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 106@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 106@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 106@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 106@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 98.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 98@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 98@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed.imageset/Group 98@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 101.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 109.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 101@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 109@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 101@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 109@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 101.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 101@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 101@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 101@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 101@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 109.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 109@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 109@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 109@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/failed_passed.imageset/Group 109@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 99.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 107.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 99@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 107@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 99@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 107@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 107.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 107.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 107@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 107@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 107@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 107@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 99.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 99@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 99@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 99@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/initial.imageset/Group 99@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 96.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 104.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 96@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 104@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 96@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 104@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 104.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 104@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 104@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 104@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 104@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 96.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 96@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 96@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 96@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/passed.imageset/Group 96@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 97.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 105.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 97@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 105@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 97@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 105@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 105.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 105.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 105@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 105@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 105@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 105@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 97.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 97@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 97@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 97@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending.imageset/Group 97@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 95.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 103.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 95@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 103@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 95@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 103@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 103.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 103@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 103@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 103@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 103@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 95.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 95@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 95@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 95@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed.imageset/Group 95@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 110.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 102.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 102@2x-1.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 102@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 110@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 110@3x-1.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 102.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 102@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 102@2x-1.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 102@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 102@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 110.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 110@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 110@3x-1.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 110@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_failed_passed.imageset/Group 110@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 100.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Group 108.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Group 100@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Group 108@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Group 100@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Group 108@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 100.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 100@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 100@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 100@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 108.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 108@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 108@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 108@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/pending_passed.imageset/Group 108@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Union.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Union@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Union@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Union.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Union.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Union@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Union@2x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Union@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_failed.imageset/Union@3x.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Union-4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Union-3.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Union@2x-4.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Union@2x-3.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Union@3x-4.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Union@3x-3.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union-3.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union-4.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@2x-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@2x-3.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@2x-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@2x-4.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@3x-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@3x-3.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@3x-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_initial.imageset/Union@3x-4.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Union-2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Union@2x-2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Union@3x-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Union-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Union-2.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Union@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Union@2x-2.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Union@3x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_passed.imageset/Union@3x-2.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Union-1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Union@2x-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Union@3x-1.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Union-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Union-1.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Union@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Union@2x-1.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Union@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/macos/cidemon-macOS/Assets.xcassets/simple_pending.imageset/Union@3x-1.png -------------------------------------------------------------------------------- /macos/cidemon-macOS/CIDemonNative.m: -------------------------------------------------------------------------------- 1 | //Author: Oscar Franco, created on: 25.05.20 2 | 3 | #import "React/RCTBridgeModule.h" 4 | 5 | @interface RCT_EXTERN_MODULE(CIDemonNative, NSObject) 6 | 7 | RCT_EXTERN_METHOD(setStatusButtonText: (NSInteger)failed running:(NSInteger)running passed:(NSInteger)passed useSimpleIcon:(BOOL)useSimpleIcon) 8 | 9 | RCT_EXTERN_METHOD(securelyStore: (NSString)key payload:(NSString)payload resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 10 | 11 | RCT_EXTERN_METHOD(securelyRetrieve: (NSString)key resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 12 | 13 | RCT_EXTERN_METHOD(sendNotification: (NSString)title payload:(NSString)payload url:(NSString)url) 14 | 15 | RCT_EXTERN_METHOD(applyAutoLauncher: (BOOL)startAtLogin) 16 | 17 | RCT_EXTERN_METHOD(closeApp) 18 | 19 | RCT_EXTERN_METHOD(requestReview) 20 | 21 | RCT_EXTERN_METHOD(showShareMenu: (NSInteger)x y:(NSInteger)y text:(NSString) text) 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/CIDemonNative.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import KeychainAccess 3 | import ServiceManagement 4 | import StoreKit 5 | import LaunchAtLogin 6 | 7 | extension Notification.Name { 8 | static let killLauncher = Notification.Name("killLauncher") 9 | } 10 | 11 | private let keychain = Keychain(service: "Tempomat Keychain") 12 | 13 | @objc(CIDemonNative) 14 | class CIDemonNative: NSObject, NSSharingServicePickerDelegate { 15 | 16 | @objc static func requiresMainQueueSetup() -> Bool { 17 | return true 18 | } 19 | 20 | 21 | @objc func setStatusButtonText(_ failed: NSInteger, running: NSInteger, passed: NSInteger, useSimpleIcon: Bool) { 22 | DispatchQueue.main.async { 23 | let appDelegate = NSApp.delegate as! AppDelegate 24 | appDelegate.setStatusText(failed: failed, running: running, passed: passed, useSimpleIcon: useSimpleIcon) 25 | } 26 | } 27 | 28 | 29 | @objc func securelyStore(_ key: NSString, payload: NSString, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { 30 | keychain[key as String] = payload as String 31 | resolver(true) 32 | } 33 | 34 | 35 | @objc func securelyRetrieve(_ key: NSString, resolver resolve: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { 36 | let value = keychain[key as String] 37 | return resolve(value) 38 | } 39 | 40 | 41 | @objc func sendNotification(_ title: NSString, payload: NSString, url: NSString) { 42 | let notification = NSUserNotification() 43 | notification.identifier = UUID().uuidString 44 | notification.subtitle = payload as String 45 | notification.title = title as String 46 | notification.userInfo = [ 47 | "url": url 48 | ] 49 | notification.soundName = "default" 50 | 51 | NSUserNotificationCenter.default.deliver(notification) 52 | } 53 | 54 | @objc func applyAutoLauncher(_ startAtLogin: Bool) { 55 | LaunchAtLogin.isEnabled = startAtLogin 56 | } 57 | 58 | @objc 59 | func closeApp() { 60 | DispatchQueue.main.async { 61 | let appDelegate = NSApp.delegate as? AppDelegate 62 | appDelegate?.closeApp() 63 | } 64 | } 65 | 66 | @objc 67 | func requestReview() { 68 | SKStoreReviewController.requestReview() 69 | } 70 | 71 | @objc 72 | func showShareMenu(_ x: NSInteger, y: NSInteger, text: NSString) { 73 | let sharingPicker = NSSharingServicePicker(items: [text]) 74 | sharingPicker.delegate = self 75 | 76 | DispatchQueue.main.async { 77 | let appDelegate = NSApp.delegate as? AppDelegate 78 | let window = appDelegate?.getWindowObject() 79 | if window != nil { 80 | sharingPicker.show(relativeTo: NSRect(x: x, y: y, width: 1, height: 1), of: window!, preferredEdge: .minY) 81 | } 82 | } 83 | } 84 | 85 | func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { 86 | guard let image = NSImage(named: NSImage.Name("copy")) else { 87 | return proposedServices 88 | } 89 | 90 | var share = proposedServices 91 | let customService = NSSharingService(title: "Copy Text", image: image, alternateImage: image, handler: { 92 | if let text = items.first as? String { 93 | NSPasteboard.general.clearContents() 94 | NSPasteboard.general.setString(text, forType: .string) 95 | } 96 | }) 97 | 98 | share.insert(customService, at: 0) 99 | 100 | return share 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ATSApplicationFontsPath 6 | Fonts 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | ITSAppUsesNonExemptEncryption 26 | 27 | LSApplicationCategoryType 28 | public.app-category.developer-tools 29 | LSMinimumSystemVersion 30 | $(MACOSX_DEPLOYMENT_TARGET) 31 | LSUIElement 32 | 33 | NSAppTransportSecurity 34 | 35 | NSAllowsArbitraryLoads 36 | 37 | NSExceptionDomains 38 | 39 | localhost 40 | 41 | NSExceptionAllowsInsecureHTTPLoads 42 | 43 | 44 | 45 | 46 | NSMainStoryboardFile 47 | Main 48 | NSPrincipalClass 49 | NSApplication 50 | NSSupportsAutomaticTermination 51 | 52 | NSSupportsSuddenTermination 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/cidemon-macOS-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/cidemon.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/lib/MyNSWindowDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyNSWindowDelegate.swift 3 | // Messer 4 | // 5 | // Created by Oscar on 07.12.21. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class MyNSWindowDelegate: NSObject, NSWindowDelegate { 12 | var resignHandler: () -> Void 13 | 14 | init(resignHandler: @escaping () -> Void) { 15 | self.resignHandler = resignHandler 16 | } 17 | 18 | func windowDidResignKey(_ notification: Notification) { 19 | resignHandler() 20 | } 21 | 22 | func windowDidResignMain(_ notification: Notification) { 23 | resignHandler() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/lib/ReactNativeBridge.swift: -------------------------------------------------------------------------------- 1 | //Author: Oscar Franco, created on: 04.07.20 2 | 3 | import Foundation 4 | 5 | class ReactNativeBridge { 6 | let bridge: RCTBridge 7 | 8 | init() { 9 | bridge = RCTBridge(delegate: ReactNativeBridgeDelegate(), launchOptions: nil) 10 | } 11 | } 12 | 13 | class ReactNativeBridgeDelegate: NSObject, RCTBridgeDelegate { 14 | 15 | func sourceURL(for bridge: RCTBridge!) -> URL! { 16 | let jsCodeLocation: URL 17 | 18 | #if DEBUG 19 | jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil) 20 | #else 21 | jsCodeLocation = Bundle.main.url(forResource: "main", withExtension: "jsbundle")! 22 | #endif 23 | return jsCodeLocation 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /macos/cidemon-macOS/lib/ReactViewController.swift: -------------------------------------------------------------------------------- 1 | //Author: Oscar Franco, created on: 04.07.20 2 | 3 | import Foundation 4 | 5 | class ReactViewController: NSViewController { 6 | init(moduleName: String, bridge: RCTBridge) { 7 | super.init(nibName: nil, bundle: nil) 8 | 9 | view = RCTRootView(bridge: bridge, moduleName: moduleName, initialProperties: nil) 10 | } 11 | 12 | required init?(coder: NSCoder) { 13 | fatalError("init(coder:) has not been implemented") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/cidemon.xcodeproj/xcshareddata/xcschemes/cidemon-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /macos/cidemon.xcodeproj/xcshareddata/xcschemes/cidemon-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /macos/cidemon.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/cidemon.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/cidemon.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "LaunchAtLogin", 6 | "repositoryURL": "git@github.com:sindresorhus/LaunchAtLogin.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "6b16bcdf7d45a9d76a768a5c4912dde925cf0e95", 10 | "version": "4.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cidemon", 3 | "version": "3.0.26", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "ios:install": "cd ios && pod install", 9 | "postinstall": "patch-package", 10 | "start": "react-native start", 11 | "test": "jest --forceExit", 12 | "test:ci": "jest --testLocationInResults --ci --outputFile=test_results.json --json", 13 | "macos": "node node_modules/react-native-macos/local-cli/cli.js run-macos", 14 | "macos:start": "node node_modules/react-native-macos/local-cli/cli.js start --use-react-native-macos", 15 | "macos:install": "cd macos && pod install", 16 | "bump": "./scripts/bumpVersion.sh" 17 | }, 18 | "dependencies": { 19 | "@react-native-picker/picker": "^1.9.8", 20 | "@react-navigation/bottom-tabs": "^6.0.7", 21 | "@react-navigation/native": "^6.0.4", 22 | "@react-navigation/stack": "^6.0.9", 23 | "autoprefixer": "^10.3.4", 24 | "axios": "^0.21.1", 25 | "base-64": "^1.0.0", 26 | "classnames": "^2.3.1", 27 | "github-api": "3.4.0", 28 | "luxon": "^1.24.1", 29 | "mobx": "6.0.4", 30 | "mobx-react-lite": "3.1.6", 31 | "postcss": "^8.3.6", 32 | "promise.allsettled": "^1.0.2", 33 | "react": "16.13.1", 34 | "react-native": "0.63.4", 35 | "react-native-get-random-values": "^1.6.0", 36 | "react-native-macos": "0.63.38", 37 | "react-native-safe-area-context": "^3.3.2", 38 | "react-native-screens": "^3.8.0", 39 | "react-native-vector-icons": "6.6.0", 40 | "tailwind-rn": "^3.0.1", 41 | "tailwindcss": "^2.2.15", 42 | "uuid": "^8.3.2" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.8.4", 46 | "@babel/runtime": "^7.8.4", 47 | "@types/jest": "^26.0.22", 48 | "@types/luxon": "^1.24.1", 49 | "@types/react-native": "^0.63.42", 50 | "@types/react-native-vector-icons": "^6.4.6", 51 | "@types/uuid": "^8.3.0", 52 | "babel-jest": "^25.1.0", 53 | "babel-plugin-module-resolver": "^4.1.0", 54 | "jest": "^25.1.0", 55 | "metro-react-native-babel-preset": "^0.59.0", 56 | "patch-package": "^6.4.7", 57 | "prettier": "2.2.1", 58 | "react-test-renderer": "16.13.1", 59 | "typescript": "4.1.3" 60 | }, 61 | "jest": { 62 | "preset": "react-native" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scripts/bumpVersion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | npm --no-git-tag-version version patch 6 | 7 | PROJECT_DIR="macos/cidemon-macOS" 8 | INFOPLIST_FILE="Info.plist" 9 | INFOPLIST_DIR="${PROJECT_DIR}/${INFOPLIST_FILE}" 10 | 11 | PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') 12 | 13 | BUILD_NUMBER=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_DIR}") 14 | BUILD_NUMBER=$(($BUILD_NUMBER + 1)) 15 | 16 | # Update plist with new values 17 | /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${PACKAGE_VERSION}" "${INFOPLIST_DIR}" 18 | /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "${INFOPLIST_DIR}" 19 | 20 | git add . 21 | 22 | git commit -m "Bump version" 23 | 24 | git tag $PACKAGE_VERSION 25 | 26 | git push -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-get-random-values'; 2 | import {decode, encode} from 'base-64'; 3 | //@ts-ignore 4 | if (!global.btoa) { 5 | //@ts-ignore 6 | global.btoa = encode; 7 | } 8 | 9 | //@ts-ignore 10 | if (!global.atob) { 11 | //@ts-ignore 12 | global.atob = decode; 13 | } 14 | 15 | import 'Theme'; 16 | import {NavigationContainer} from '@react-navigation/native'; 17 | import {ToastContainer} from 'container'; 18 | import React, {useEffect, useState} from 'react'; 19 | import {LogBox, Platform} from 'react-native'; 20 | import {createRootStore, IRootStore, StoreProvider} from 'Root.store'; 21 | import {Routes} from 'Route'; 22 | 23 | LogBox.ignoreLogs([ 24 | `Warning: componentWillReceiveProps`, 25 | `Calling \`getNode`, 26 | `Module RNBackgroundFetch`, 27 | `RCTBridge required dispatch_sync`, 28 | `currentlyFocusedField is deprecated`, 29 | `focusTextInput must be called with a host component`, 30 | ]); 31 | 32 | const theme = { 33 | colors: { 34 | background: 'transparent', 35 | }, 36 | dark: false, 37 | }; 38 | 39 | export function App() { 40 | let [rootStore, setRootStore] = useState(null); 41 | useEffect(() => { 42 | createRootStore().then(setRootStore); 43 | }, []); 44 | 45 | if (!rootStore) { 46 | return null; 47 | } 48 | 49 | return ( 50 | 51 | {/* @ts-expect-error */} 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/Assets.ts: -------------------------------------------------------------------------------- 1 | import circleci from './assets/icons/circleci.png'; 2 | import appcenter from './assets/icons/appcenter.png'; 3 | import bitrise from './assets/icons/bitrise.png'; 4 | import github from './assets/icons/github.png'; 5 | import travisci from './assets/icons/travisci.png'; 6 | import gitlab from './assets/icons/gitlab.png'; 7 | import github_bar from './assets/icons/github_bar.png'; 8 | import logo from './assets/icons/logo.png'; 9 | import profile from './assets/image/profile.jpeg'; 10 | 11 | export const Images: Record = { 12 | circleci, 13 | appcenter, 14 | bitrise, 15 | github, 16 | travisci, 17 | gitlab, 18 | logo, 19 | github_bar, 20 | profile, 21 | }; 22 | -------------------------------------------------------------------------------- /src/Root.store.ts: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from 'react'; 2 | import {createApiStore, createNodeStore, createUIStore} from 'store'; 3 | 4 | export interface IRootStore { 5 | api: ReturnType; 6 | ui: ReturnType; 7 | node: Awaited>; 8 | } 9 | 10 | export let createRootStore = async (): Promise => { 11 | let store: any = {}; 12 | store.ui = createUIStore(store); 13 | store.api = createApiStore(store); 14 | store.node = await createNodeStore(store); 15 | 16 | return store; 17 | }; 18 | 19 | // Placeholder value for the context, is replaced as soon as store is re-hydrated on renderer.tsx 20 | export let StoreContext = createContext(null as any); 21 | export let StoreProvider = StoreContext.Provider; 22 | export let useStore = () => useContext(StoreContext); 23 | -------------------------------------------------------------------------------- /src/Route.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createStackNavigator} from '@react-navigation/stack'; 3 | import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; 4 | import { 5 | NodeListContainer, 6 | AddTokenContainer, 7 | IgnoreConfigContainer, 8 | GithubActionsConfigContainer, 9 | GeneralConfigContainer, 10 | } from 'container'; 11 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 12 | import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome5'; 13 | import {useDarkTheme} from 'lib'; 14 | import {tw, cw} from 'tailwind'; 15 | 16 | export type IRootStackParams = { 17 | Home: undefined; 18 | Configuration: undefined; 19 | AddToken: undefined; 20 | PollingIntervalConfig: undefined; 21 | }; 22 | 23 | let RootStack = createStackNavigator(); 24 | let ConfigurationTabStack = createBottomTabNavigator(); 25 | 26 | const ConfigurationRoutes = () => { 27 | let isDarkMode = useDarkTheme(); 28 | 29 | return ( 30 | 42 | ( 47 | 52 | ), 53 | title: ``, 54 | }} 55 | /> 56 | ( 61 | 66 | ), 67 | title: ``, 68 | }} 69 | /> 70 | {/* ( 75 | 80 | ), 81 | title: ``, 82 | }} 83 | /> */} 84 | ( 89 | 94 | ), 95 | title: ``, 96 | }} 97 | /> 98 | 99 | ); 100 | }; 101 | 102 | export const Routes = () => { 103 | let isDarkMode = useDarkTheme(); 104 | return ( 105 | 114 | 115 | 116 | 117 | 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /src/Theme.ts: -------------------------------------------------------------------------------- 1 | global.colors = { 2 | blue800: `#37474F`, 3 | gray1000: `#282929`, 4 | gray900: `#313232`, 5 | gray800: `#444`, 6 | gray500: `#555`, 7 | gray400: `#777`, 8 | gray300: `#AAA`, 9 | gray200: `#CCC`, 10 | gray100: `#DDD`, 11 | gray050: `#EEE`, 12 | gray010: `#F3F3F3`, 13 | blue600: `#008dd9`, 14 | blue500: `#00A4FF`, 15 | blue400: `#00A5F8`, 16 | blue300: `#00cbf8`, 17 | green500: `#27CA42`, 18 | red500: `#FC4943`, 19 | orange500: `#FFBE30`, 20 | } 21 | 22 | global.metrics = { 23 | imgSmall: 18, 24 | imgMedium: 24, 25 | imgLarge: 32, 26 | pxs: 3, 27 | ps: 6, 28 | pm: 8, 29 | pxm: 12, 30 | pl: 16, 31 | ts: 14, 32 | tm: 16, 33 | tl: 18, 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/icons/appcenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/appcenter.png -------------------------------------------------------------------------------- /src/assets/icons/bitrise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/bitrise.png -------------------------------------------------------------------------------- /src/assets/icons/circleci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/circleci.png -------------------------------------------------------------------------------- /src/assets/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/github.png -------------------------------------------------------------------------------- /src/assets/icons/github_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/github_bar.png -------------------------------------------------------------------------------- /src/assets/icons/github_bar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/github_bar@2x.png -------------------------------------------------------------------------------- /src/assets/icons/github_bar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/github_bar@3x.png -------------------------------------------------------------------------------- /src/assets/icons/gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/gitlab.png -------------------------------------------------------------------------------- /src/assets/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/logo.png -------------------------------------------------------------------------------- /src/assets/icons/travisci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/icons/travisci.png -------------------------------------------------------------------------------- /src/assets/image/profile.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/assets/image/profile.jpeg -------------------------------------------------------------------------------- /src/component/Divider.component.tsx: -------------------------------------------------------------------------------- 1 | import {useDynamic} from 'lib'; 2 | import React from 'react'; 3 | import {View} from 'react-native'; 4 | import tw from 'tailwind-rn'; 5 | 6 | interface IProps { 7 | width?: number | string; 8 | alignSelf?: 9 | | `auto` 10 | | `flex-start` 11 | | `flex-end` 12 | | `center` 13 | | `stretch` 14 | | `baseline` 15 | | undefined; 16 | style?: any; 17 | } 18 | 19 | export const Divider = ({width = `100%`, alignSelf, style}: IProps) => { 20 | const dynamic = useDynamic(); 21 | return ( 22 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/component/EmptyNodes.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, StyleSheet, Image, ActivityIndicator} from 'react-native'; 3 | import {TempoButton} from './TempoButton.component'; 4 | import {Images} from 'Assets'; 5 | import {observer} from 'mobx-react-lite'; 6 | import {useStore} from 'Root.store'; 7 | import {tw} from 'tailwind'; 8 | 9 | interface IProps { 10 | onAddToken: () => void; 11 | } 12 | 13 | export const EmptyNodesComponent = observer(({onAddToken}: IProps) => { 14 | const root = useStore(); 15 | const {fetching} = root.node; 16 | 17 | return ( 18 | 19 | {!fetching && !!root.node.tokens.length && ( 20 | Something might have gone wrong :( 21 | )} 22 | 23 | {!root.node.tokens.length && ( 24 | 29 | )} 30 | 31 | {fetching && Fetching} 32 | 33 | {!root.node.tokens.length && ( 34 | 40 | )} 41 | 42 | ); 43 | }); 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | height: `100%`, 48 | alignItems: `center`, 49 | justifyContent: `center`, 50 | }, 51 | title: {fontWeight: `bold`, paddingVertical: 10}, 52 | }); 53 | -------------------------------------------------------------------------------- /src/component/NodeRow.component.tsx: -------------------------------------------------------------------------------- 1 | import {Images} from 'Assets'; 2 | import {TINT_MAPPING, useBoolean, useDarkTheme, useDynamic} from 'lib'; 3 | import {observer} from 'mobx-react-lite'; 4 | import React, {useEffect, useState} from 'react'; 5 | import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; 6 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 7 | import {tw} from 'tailwind'; 8 | import {Row} from './Row.component'; 9 | 10 | let captureBranchRegex = /(.*)(\[.*\])(.*)/; 11 | 12 | interface IProps { 13 | node: INode; 14 | onPress: () => void; 15 | style?: any; 16 | selected?: boolean; 17 | } 18 | 19 | export const NodeRow = observer(({node, onPress, style, selected}: IProps) => { 20 | const [hovered, onHover, offHover] = useBoolean(); 21 | const [tokens, setTokens] = useState(null); 22 | const dynamic = useDynamic(); 23 | const isDark = useDarkTheme(); 24 | 25 | useEffect(() => { 26 | const newTokens = captureBranchRegex.exec(node.label); 27 | if (newTokens && newTokens.length > 1) { 28 | setTokens(newTokens); 29 | } 30 | }, [node.label]); 31 | 32 | let icon = Images[`${node.source.toLowerCase()}`]; 33 | 34 | let tintColor = TINT_MAPPING[node.status]; 35 | 36 | let textColor = dynamic(`text-gray-100`, `text-gray-800`); 37 | let hoverColor = dynamic(`bg-gray-700`, `bg-blue-100`); 38 | 39 | let text; 40 | 41 | if (tokens) { 42 | text = ( 43 | 44 | {tokens[2].substring(1, tokens[2].length - 1)} 45 | 46 | ); 47 | } else { 48 | text = {node.label}; 49 | } 50 | 51 | return ( 52 | 58 | 59 | 68 | 69 | {!!icon && ( 70 | 71 | 72 | 73 | )} 74 | 75 | {text} 76 | 77 | {node.isAction && ( 78 | 79 | ACTION 80 | 81 | )} 82 | 83 | {node.isPr && ( 84 | <> 85 | PR 86 | 87 | 88 | )} 89 | 90 | {node.isBranch && ( 91 | 92 | )} 93 | 94 | {!!node.userAvatarUrl && ( 95 | 99 | )} 100 | 101 | 102 | {/* Sub nodes */} 103 | {node.status === `failed` && node.subItems && ( 104 | 105 | {node.subItems.map((subItem: ISubNode, index: number, items) => ( 106 | 107 | 108 | 117 | 123 | 124 | 135 | 136 | 137 | 143 | {subItem.label} 144 | {!!subItem.extraLabel && ( 145 | · {subItem.extraLabel} 146 | )} 147 | 148 | 149 | ))} 150 | 151 | )} 152 | 153 | 154 | 155 | ); 156 | }); 157 | 158 | const styles = StyleSheet.create({ 159 | imageIcon: { 160 | height: 24, 161 | width: 24, 162 | resizeMode: `contain`, 163 | }, 164 | icon: { 165 | marginLeft: global.metrics.ps, 166 | marginRight: global.metrics.pm, 167 | fontSize: global.metrics.tl, 168 | }, 169 | statusIndicator: { 170 | width: 8, 171 | height: 8, 172 | borderRadius: 4, 173 | }, 174 | }); 175 | -------------------------------------------------------------------------------- /src/component/Row.component.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | import {View} from 'react-native'; 3 | 4 | interface IProps { 5 | style?: any; // StyleProp is breaking here with paddingHorizontal... for some reason 6 | horizontal?: 7 | | `flex-end` 8 | | `flex-start` 9 | | `center` 10 | | `space-around` 11 | | `space-between`; 12 | wrap?: boolean; 13 | vertical?: `center` | `flex-start` | `flex-end` | `space-between`; 14 | children?: any; 15 | } 16 | 17 | export const Row = memo( 18 | ({ 19 | style = {}, 20 | horizontal = `flex-start`, 21 | vertical = `flex-start`, 22 | wrap, 23 | children, 24 | }: IProps) => { 25 | let innerStyle = { 26 | display: `flex`, 27 | flexDirection: `row`, 28 | justifyContent: horizontal, 29 | alignItems: vertical, 30 | flexWrap: wrap ? `wrap` : `nowrap`, 31 | }; 32 | 33 | return {children}; 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /src/component/Spacer.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {View} from "react-native" 3 | 4 | export const Spacer = () => 5 | -------------------------------------------------------------------------------- /src/component/TempoButton.component.tsx: -------------------------------------------------------------------------------- 1 | import {useBoolean, useDarkTheme} from 'lib'; 2 | import React, {useState} from 'react'; 3 | import { 4 | TouchableOpacity, 5 | Text, 6 | StyleSheet, 7 | GestureResponderEvent, 8 | } from 'react-native'; 9 | import {tw} from 'tailwind'; 10 | 11 | interface IProps { 12 | onPress?: () => void; 13 | title?: string; 14 | primary?: boolean; 15 | secondary?: boolean; 16 | style?: any; 17 | children?: any; 18 | applyMargin?: boolean; 19 | onPressWithPosition?: (x: number, y: number) => void; 20 | } 21 | 22 | export const TempoButton = ({ 23 | onPress, 24 | title, 25 | primary, 26 | secondary, 27 | style, 28 | children, 29 | applyMargin, 30 | onPressWithPosition, 31 | }: IProps) => { 32 | const [hovered, onHover, offHover] = useBoolean(); 33 | const [coordinate, setCoordinate] = useState(); 34 | const isDark = useDarkTheme(); 35 | const _onPress = (e: GestureResponderEvent) => { 36 | e.preventDefault(); 37 | e.stopPropagation(); 38 | onPress?.(); 39 | onPressWithPosition?.( 40 | coordinate.x + coordinate.width, 41 | coordinate.y + coordinate.height, 42 | ); 43 | }; 44 | 45 | if (secondary) { 46 | return ( 47 | setCoordinate(e.nativeEvent.layout)} 50 | // @ts-ignore 51 | enableFocusRing={false} 52 | style={[styles.secondaryButton, applyMargin && styles.margin, style]}> 53 | {!!title && {title}} 54 | {children} 55 | 56 | ); 57 | } 58 | 59 | if (primary) { 60 | return ( 61 | { 63 | e.preventDefault(); 64 | e.stopPropagation(); 65 | }} 66 | onPress={_onPress} 67 | onPressOut={(e) => { 68 | e.preventDefault(); 69 | e.stopPropagation(); 70 | }} 71 | onLayout={(e) => setCoordinate(e.nativeEvent.layout)} 72 | // @ts-ignore 73 | enableFocusRing={false} 74 | onMouseEnter={onHover} 75 | onMouseLeave={offHover} 76 | style={[ 77 | tw( 78 | { 79 | 'bg-sky-500': hovered, 80 | 'bg-blue-500': !hovered, 81 | }, 82 | `px-3 py-1 items-center rounded`, 83 | ), 84 | style, 85 | ]}> 86 | {!!title && {title}} 87 | {children} 88 | 89 | ); 90 | } 91 | 92 | return ( 93 | setCoordinate(e.nativeEvent.layout)} 96 | // @ts-ignore 97 | enableFocusRing={false} 98 | onMouseEnter={onHover} 99 | onMouseLeave={offHover} 100 | style={[ 101 | tw( 102 | { 103 | 'bg-gray-100': !isDark && hovered, 104 | 'bg-gray-700': isDark && hovered, 105 | }, 106 | 'px-2 py-1 items-center bg-opacity-30 rounded', 107 | ), 108 | style, 109 | ]}> 110 | {!!title && {title}} 111 | {children} 112 | 113 | ); 114 | }; 115 | 116 | const styles = StyleSheet.create({ 117 | flatButton: { 118 | color: global.colors.blue500, 119 | fontSize: 14, 120 | }, 121 | secondaryButton: { 122 | paddingHorizontal: global.metrics.pl, 123 | paddingVertical: global.metrics.ps, 124 | //@ts-ignore 125 | backgroundColor: { 126 | dynamic: { 127 | dark: `#6E6F6F`, 128 | light: `#DDD`, 129 | }, 130 | }, 131 | alignItems: `center`, 132 | borderRadius: 7, 133 | }, 134 | margin: { 135 | margin: global.metrics.pl, 136 | }, 137 | }); 138 | -------------------------------------------------------------------------------- /src/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Row.component'; 2 | export * from './Spacer.component'; 3 | export * from './Divider.component'; 4 | export * from './TempoButton.component'; 5 | export * from './NodeRow.component'; 6 | export * from './EmptyNodes.component'; 7 | -------------------------------------------------------------------------------- /src/container/AddToken.container.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef, useMemo} from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | ScrollView, 7 | StyleSheet, 8 | TouchableOpacity, 9 | } from 'react-native'; 10 | import {Picker} from '@react-native-picker/picker'; 11 | import {StackNavigationProp} from '@react-navigation/stack'; 12 | import {Row, Spacer, Divider, TempoButton} from 'component'; 13 | import {observer} from 'mobx-react-lite'; 14 | import {useStore} from 'Root.store'; 15 | import {IRootStackParams} from 'Route'; 16 | import {tw} from 'tailwind'; 17 | import {useDarkTheme, useDynamic, REGEX_VALID_URL} from 'lib'; 18 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 19 | 20 | interface IProps { 21 | navigation: StackNavigationProp; 22 | } 23 | 24 | const sources: Source[] = [ 25 | `AppCenter`, 26 | `Bitrise`, 27 | `CircleCI`, 28 | `Gitlab`, 29 | `TravisCI`, 30 | ]; 31 | 32 | export const AddTokenContainer = observer(({navigation}: IProps) => { 33 | let root = useStore(); 34 | let [source, setSource] = useState(`CircleCI`); 35 | let [name, setName] = useState(``); 36 | let [key, setKey] = useState(``); 37 | let [baseURL, setBaseURL] = useState(`https://gitlab.com`); 38 | let [visibility, setVisibility] = useState(`private`); 39 | let secondField = useRef(); 40 | let thirdField = useRef(); 41 | let dynamic = useDynamic(); 42 | let isDark = useDarkTheme(); 43 | 44 | let isGitlab = useMemo(() => { 45 | if(source === 'Gitlab'){ 46 | return true; 47 | } 48 | return false; 49 | }, [source]); 50 | 51 | let inputFieldStyle = tw(`p-3 ${dynamic(`bg-gray-900`, `bg-gray-100`)}`); 52 | 53 | function addToken() { 54 | if (!name) { 55 | root.ui.addToast({ 56 | text: 'Please add a name', 57 | type: 'error', 58 | position: 'top', 59 | }); 60 | return; 61 | } 62 | 63 | if (!key) { 64 | root.ui.addToast({ 65 | text: 'Please add a key', 66 | type: 'error', 67 | position: 'top', 68 | }); 69 | return; 70 | } 71 | 72 | if (isGitlab) { 73 | if(!(baseURL && REGEX_VALID_URL.test(baseURL))){ 74 | root.ui.addToast({ 75 | text: 'Please add a valid base URL', 76 | type: 'error', 77 | position: 'top', 78 | }); 79 | return; 80 | } 81 | 82 | root.node.addToken(source, name, key, baseURL, visibility); 83 | } else { 84 | root.node.addToken(source, name, key); 85 | } 86 | 87 | navigation.goBack(); 88 | } 89 | 90 | return ( 91 | 92 | 93 | navigation.goBack()}> 94 | ← Add Token 95 | 96 | TOKEN NAME 97 | isGitlab ? secondField.current?.focus() : thirdField.current?.focus()} 117 | blurOnSubmit={false} 118 | /> 119 | 120 | CI PROVIDER 121 | 122 | 130 | {sources.map((item, ii) => { 131 | return ( 132 | 133 | setSource(item)}> 134 | 136 | 137 | 139 | {item} 140 | 141 | 142 | {item === source && ( 143 | 144 | )} 145 | 146 | 147 | 148 | {ii !== sources.length - 1 && } 149 | 150 | ); 151 | })} 152 | 153 | {isGitlab ? ( 154 | <> 155 | HOST DOMAIN 156 | 157 | 158 | {`You can replace https://gitlab.com below with your own instance domain if you are using a self-managed Gitlab instance.`} 159 | 160 | 161 | thirdField.current?.focus()} 176 | /> 177 | 178 | VISIBILITY 179 | 187 | 188 | 189 | Project visibility of repositories to observe 190 | 191 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | ) : null} 205 | KEY 206 | {/* @ts-ignore */} 207 | 229 | 230 | 236 | 237 | 238 | ); 239 | }); 240 | 241 | const styles = StyleSheet.create({ 242 | macOSSwitch: { 243 | height: 15, 244 | width: 15, 245 | }, 246 | }); 247 | -------------------------------------------------------------------------------- /src/container/GeneralConfig.container.tsx: -------------------------------------------------------------------------------- 1 | import {Picker} from '@react-native-picker/picker'; 2 | import {StackNavigationProp} from '@react-navigation/stack'; 3 | import {Images} from 'Assets'; 4 | import {Divider, Row, Spacer} from 'component'; 5 | import {useDarkTheme, useDynamic} from 'lib'; 6 | import {observer} from 'mobx-react-lite'; 7 | import React from 'react'; 8 | import { 9 | Image, 10 | StyleSheet, 11 | Switch, 12 | Text, 13 | TouchableOpacity, 14 | View, 15 | ScrollView, 16 | Linking, 17 | } from 'react-native'; 18 | import FoIcon from 'react-native-vector-icons/FontAwesome'; 19 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 20 | import {useStore} from 'Root.store'; 21 | import {IRootStackParams} from 'Route'; 22 | import {tw} from 'tailwind'; 23 | // @ts-ignore 24 | import {version} from '../../package.json'; 25 | 26 | interface IProps { 27 | navigation: StackNavigationProp; 28 | } 29 | 30 | export let GeneralConfigContainer = observer(({navigation}: IProps) => { 31 | let root = useStore(); 32 | let {tokens} = root.node; 33 | let dynamic = useDynamic(); 34 | const isDark = useDarkTheme(); 35 | 36 | let settingsRowStyle = tw(``); 37 | 38 | return ( 39 | 40 | 41 | navigation.popToTop()}> 42 | ← Settings 43 | 44 | 45 | TOKENS 46 | 47 | 55 | {!tokens.length && ( 56 | 57 | No saved tokens 58 | 59 | )} 60 | 61 | 62 | 63 | {tokens.map((t, idx) => { 64 | return ( 65 | 69 | 85 | {t.name} 86 | 87 | 91 | root.node.removeTokenByName(t.name)} 95 | /> 96 | 97 | 98 | ); 99 | })} 100 | 101 | navigation.navigate(`AddToken`)}> 102 | 103 | 104 | Add Token 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | GENERAL 113 | 114 | 122 | 123 | 124 | Polling Interval 125 | (minutes) 126 | 127 | root.node.setFetchInterval(v as any)}> 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | Failing builds notifications 144 | 145 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | Passing builds notifications 158 | 159 | 164 | 165 | 166 | 167 | 168 | 169 | Show build number 170 | 171 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | Simple status bar icon 183 | 184 | 189 | 190 | 191 | 192 | 193 | 194 | Launch on login 195 | 196 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | Report an issue 208 | 209 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | Quit 222 | 223 | 228 | 229 | 230 | 231 | 232 | 233 | {/* 234 | 235 | Two row item names 236 | 237 | 242 | 243 | {global.isMacOS && } 244 | */} 245 | 246 | {/* {__DEV__ && ( 247 | 248 | DEBUG BUILD COMMANDS 249 | { 252 | cidemonNative.sendNotification(`Test`, `Payload`, `URL`); 253 | }} 254 | /> 255 | { 258 | root.ui.addToast({ 259 | text: `test notification`, 260 | type: 'success', 261 | }); 262 | }} 263 | /> 264 | { 267 | root.ui.addToast({ 268 | text: `test fail`, 269 | type: 'error', 270 | }); 271 | }} 272 | /> 273 | 274 | )} */} 275 | 276 | 277 | 278 | CI Demon 279 | v{version} 280 | 281 | { 284 | Linking.openURL(`https://ospfranco.com/`); 285 | }}> 286 | Oscar Franco 287 | 288 | · 289 | { 292 | Linking.openURL( 293 | `https://apps.apple.com/de/app/ci-demon/id1560355863?l=en&mt=12`, 294 | ); 295 | }}> 296 | Share 297 | 298 | 299 | 300 | 301 | 302 | 303 | ); 304 | }); 305 | 306 | const styles = StyleSheet.create({ 307 | buttonContainer: { 308 | justifyContent: `space-around`, 309 | paddingVertical: 40, 310 | }, 311 | switch: { 312 | height: 15, 313 | width: 15, 314 | }, 315 | settingsRowInternal: { 316 | paddingVertical: global.metrics.pxm, 317 | paddingHorizontal: global.metrics.pl, 318 | }, 319 | }); 320 | -------------------------------------------------------------------------------- /src/container/GithubActionsConfig.container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Text, 4 | TextInput, 5 | ScrollView, 6 | TouchableOpacity, 7 | StyleSheet, 8 | View, 9 | Linking, 10 | Switch, 11 | } from 'react-native'; 12 | import {observer} from 'mobx-react-lite'; 13 | import {useStore} from 'Root.store'; 14 | import {Divider, Row, Spacer, TempoButton} from 'component'; 15 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 16 | import {useDarkTheme, useDynamic} from 'lib'; 17 | import {tw} from 'tailwind'; 18 | 19 | let placeHolderStyle: any = { 20 | dynamic: { 21 | dark: global.colors.gray200, 22 | light: global.colors.gray500, 23 | }, 24 | }; 25 | 26 | function openGithubGuide() { 27 | Linking.openURL( 28 | `https://ospfranco.github.io/post/2021/05/08/how-to-add-a-github-token-to-ci-demon/`, 29 | ); 30 | } 31 | 32 | export const GithubActionsConfigContainer = observer(({navigation}: any) => { 33 | let root = useStore(); 34 | let dynamic = useDynamic(); 35 | const isDark = useDarkTheme(); 36 | 37 | let inputFieldStyle = tw(`p-3 ${dynamic(`bg-gray-900`, `bg-gray-100`)}`); 38 | let settingsRowStyle = tw(''); 39 | 40 | return ( 41 | 42 | 43 | navigation.popToTop()}> 44 | ← Github 45 | 46 | 47 | 48 | {`In order to connect with github, first you need to generate an access token and then subscribe to each repository individually (sorry! API limitations).`} 49 | 50 | 51 | 52 | How do I create an Access Token? 53 | 54 | 55 | {/* */} 59 | 60 | ACCESS TOKEN 61 | 75 | 76 | ITEMS TO FETCH 77 | 85 | 86 | 87 | 88 | Fetch pull requests 89 | 90 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Fetch branches 102 | 103 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Fetch workflows 115 | 116 | 120 | 121 | 122 | 123 | 124 | 125 | OBSERVED REPOSITORIES 126 | 127 | {root.node.githubRepos.map((r, ii) => { 128 | return ( 129 | 130 | root.node.setGithubRepoAtIndex(t, ii)} 142 | /> 143 | 144 | root.node.deleteGithubRepo(ii)}> 145 | 149 | 150 | 151 | 152 | 153 | ); 154 | })} 155 | 156 | 157 | 163 | 164 | 165 | 166 | 167 | ); 168 | }); 169 | 170 | // @ts-ignore 171 | GithubActionsConfigContainer.navigationOptions = () => ({ 172 | title: `Github Actions`, 173 | }); 174 | 175 | const styles = StyleSheet.create({ 176 | container: { 177 | flex: 1, 178 | // @ts-ignore 179 | backgroundColor: { 180 | dynamic: { 181 | light: global.colors.gray010, 182 | dark: global.colors.gray800, 183 | }, 184 | }, 185 | }, 186 | contentContainer: {}, 187 | listContainer: { 188 | flex: 1, 189 | // @ts-ignore 190 | backgroundColor: { 191 | dynamic: { 192 | light: global.colors.gray010, 193 | dark: global.colors.gray900, 194 | }, 195 | }, 196 | }, 197 | row: { 198 | padding: global.metrics.pl, 199 | //@ts-ignore 200 | backgroundColor: { 201 | dynamic: { 202 | light: `white`, 203 | dark: `#1E1E1E`, 204 | }, 205 | }, 206 | }, 207 | repositoryField: { 208 | width: `90%`, 209 | height: 20, 210 | borderWidth: 0, 211 | padding: 0, 212 | // @ts-ignore 213 | color: { 214 | dynamic: { 215 | light: `black`, 216 | dark: `white`, 217 | }, 218 | }, 219 | }, 220 | }); 221 | -------------------------------------------------------------------------------- /src/container/IgnoreConfig.container.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { 3 | View, 4 | Text, 5 | FlatList, 6 | TextInput, 7 | StyleSheet, 8 | Switch, 9 | TouchableOpacity, 10 | } from 'react-native'; 11 | import {Divider, Row, Spacer, TempoButton} from 'component'; 12 | import {useStore} from 'Root.store'; 13 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 14 | import {observer} from 'mobx-react-lite'; 15 | import {tw} from 'tailwind'; 16 | import {useDarkTheme, useDynamic} from 'lib'; 17 | import {StackNavigationProp} from '@react-navigation/stack'; 18 | import {IRootStackParams} from 'Route'; 19 | 20 | let placeHolderStyle: any = { 21 | dynamic: { 22 | dark: global.colors.gray400, 23 | light: global.colors.gray500, 24 | }, 25 | }; 26 | 27 | interface IProps { 28 | navigation: StackNavigationProp; 29 | } 30 | 31 | export const IgnoreConfigContainer = observer(({navigation}: IProps) => { 32 | let root = useStore(); 33 | let [regex, setRegex] = useState(``); 34 | let [inverted, setInverted] = useState(false); 35 | let [id, setId] = useState(null); 36 | let [error, setError] = useState(false); 37 | const isDark = useDarkTheme(); 38 | const dynamic = useDynamic(); 39 | 40 | function commit() { 41 | if (id) { 42 | setId(null); 43 | setRegex(``); 44 | setInverted(false); 45 | 46 | let tempRegex = regex.trim(); 47 | 48 | root.node.updateIgnoredRegex(id, tempRegex, inverted); 49 | } else { 50 | let tempRegex = regex.trim(); 51 | if (tempRegex !== ``) { 52 | let inserted = root.node.addIgnoredRegex(tempRegex, inverted); 53 | if (!inserted) { 54 | setError(true); 55 | } else { 56 | setRegex(``); 57 | } 58 | } 59 | } 60 | } 61 | 62 | return ( 63 | 64 | 65 | navigation.popToTop()}> 66 | ← Filters 67 | 68 | 69 | IGNORED REGEXES 70 | ( 73 | 74 | 79 | {info.item.regex} 80 | 81 | {info.item.inverted && ( 82 | Inverted 83 | )} 84 | 85 | { 90 | setId(info.item.id); 91 | setRegex(info.item.regex); 92 | setInverted(info.item.inverted); 93 | }} 94 | /> 95 | 96 | root.node.removeIgnoredRegex(info.item.regex)} 101 | /> 102 | 103 | 104 | 105 | )} 106 | keyExtractor={(t) => t.regex} 107 | contentContainerStyle={tw(`flex-grow`)} 108 | ListEmptyComponent={ 109 | 110 | 111 | No ignored patterns 112 | 113 | 114 | } 115 | style={tw( 116 | { 117 | 'bg-white': !isDark, 118 | 'bg-gray-900': isDark, 119 | }, 120 | 'rounded-lg bg-opacity-70 mb-3', 121 | )} 122 | /> 123 | 124 | NEW REGEX 125 | 126 | 127 | 128 | 141 | {error && Not a valid regex} 142 | 143 | Inverted 144 | (Show matching branches) 145 | setInverted(!inverted)} 148 | style={tw(`w-6`)} 149 | /> 150 | 151 | 152 | 158 | {!!id && ( 159 | { 162 | setId(null); 163 | setRegex(``); 164 | setInverted(false); 165 | }} 166 | /> 167 | )} 168 | 169 | 170 | 171 | ); 172 | }); 173 | 174 | // @ts-ignore 175 | IgnoreConfigContainer.navigationOptions = () => ({ 176 | title: `Ignore`, 177 | }); 178 | 179 | let styles = StyleSheet.create({ 180 | container: { 181 | flex: 1, 182 | // @ts-ignore 183 | backgroundColor: { 184 | dynamic: { 185 | light: global.colors.gray010, 186 | dark: global.colors.gray800, 187 | }, 188 | }, 189 | }, 190 | row: { 191 | flexDirection: `row`, 192 | alignItems: `center`, 193 | paddingHorizontal: global.metrics.pl, 194 | //@ts-ignore 195 | backgroundColor: { 196 | dynamic: { 197 | light: `white`, 198 | dark: global.colors.gray900, 199 | }, 200 | }, 201 | }, 202 | list: { 203 | flex: 1, 204 | //@ts-ignore 205 | backgroundColor: { 206 | dynamic: { 207 | light: global.colors.gray010, 208 | dark: global.colors.gray900, 209 | }, 210 | }, 211 | }, 212 | input: { 213 | paddingVertical: global.metrics.pm, 214 | // @ts-ignore 215 | color: { 216 | dynamic: { 217 | light: `black`, 218 | dark: `white`, 219 | }, 220 | }, 221 | }, 222 | editIcon: { 223 | // @ts-ignore 224 | color: { 225 | dynamic: { 226 | light: global.colors.blue500, 227 | dark: `white`, 228 | }, 229 | }, 230 | paddingRight: 10, 231 | }, 232 | emptyContainer: {justifyContent: `center`, height: 200}, 233 | flex1: { 234 | flex: 1, 235 | }, 236 | header: { 237 | paddingTop: global.metrics.pl * 2, 238 | padding: global.metrics.pl, 239 | fontWeight: `500`, 240 | }, 241 | }); 242 | -------------------------------------------------------------------------------- /src/container/NodeList.container.tsx: -------------------------------------------------------------------------------- 1 | import {StackNavigationProp} from '@react-navigation/stack'; 2 | import {Images} from 'Assets'; 3 | import { 4 | EmptyNodesComponent, 5 | NodeRow, 6 | Row, 7 | Spacer, 8 | TempoButton, 9 | } from 'component'; 10 | import {idExtractor, useDarkTheme} from 'lib'; 11 | import {observer} from 'mobx-react-lite'; 12 | import React from 'react'; 13 | import { 14 | Image, 15 | Linking, 16 | ListRenderItemInfo, 17 | SectionList, 18 | Text, 19 | TouchableOpacity, 20 | View, 21 | } from 'react-native'; 22 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 23 | import {useStore} from 'Root.store'; 24 | import {IRootStackParams} from 'Route'; 25 | import {tw} from 'tailwind'; 26 | 27 | interface IProps { 28 | navigation: StackNavigationProp; 29 | } 30 | 31 | export let NodeListContainer = observer(({navigation}: IProps) => { 32 | let root = useStore(); 33 | let isDark = useDarkTheme(); 34 | 35 | let iconStyle = [ 36 | tw(`${isDark ? `text-white` : ``} text-base`), 37 | {lineHeight: 16}, 38 | ]; 39 | 40 | let goToAddTokenScreen = () => { 41 | navigation.navigate(`AddToken`); 42 | }; 43 | 44 | const renderNodeItem = ({item}: ListRenderItemInfo) => { 45 | return ( 46 | { 49 | root.node.openNode(item.url); 50 | }} 51 | /> 52 | ); 53 | }; 54 | 55 | const sectionsObj = root.node.sortedFilteredNodes.reduce( 56 | (acc: any, node: INode) => { 57 | if (node.slug) { 58 | if (acc[node.slug]) { 59 | acc[node.slug].push(node); 60 | } else { 61 | acc[node.slug] = [node]; 62 | } 63 | } else { 64 | if (acc['No repository']) { 65 | acc['No repository'].push(node); 66 | } else { 67 | acc['No repository'] = [node]; 68 | } 69 | } 70 | 71 | return acc; 72 | }, 73 | {}, 74 | ); 75 | 76 | const sections: Array<{title: string; data: INode[]}> = Object.entries( 77 | sectionsObj, 78 | ).map(([slug, data]) => ({ 79 | title: slug, 80 | data, 81 | })) as any; 82 | 83 | return ( 84 | 89 | 90 | 96 | navigation.navigate(`Configuration`)} 98 | style={tw('py-1 px-3 rounded', { 99 | 'bg-gray-100': !isDark, 100 | 'bg-gray-700': isDark, 101 | })}> 102 | 103 | 104 | 105 | 106 | 112 | 117 | Filters:{' '} 118 | 119 | {root.node.filterHardOffSwitch || 120 | !root.node.complexRegexes.length 121 | ? `Off` 122 | : `On`} 123 | 124 | 125 | 126 | 127 | 128 | 129 | 135 | 136 | Sort:{' '} 137 | {root.node.sortingKey} 138 | 139 | 140 | 141 | 142 | 143 | 147 | 153 | 154 | 155 | ( 162 | 170 | {title} 171 | 172 | )} 173 | ListEmptyComponent={ 174 | 175 | } 176 | /> 177 | 178 | 179 | {/* Welcome */} 180 | {!root.node.welcomeShown && ( 181 | 189 | 193 | 194 | 195 | 196 | Welcome to CI Demon. 197 | 198 | 199 | 200 | Start by generating an access token on your CI, then save it the 201 | settings. Afterwards, your repositories and CI Jobs will be 202 | automatically polled. 203 | 204 | 205 | 206 | Reviewing the app helps 207 | me create more tools, same for following me on Twitter. 208 | 209 | Thanks a lot! 210 | 211 | 212 | 213 | { 217 | Linking.openURL('https://twitter.com/ospfranco'); 218 | root.node.dismissWelcome(); 219 | }}> 220 | 221 | )} 222 | 223 | ); 224 | }); 225 | -------------------------------------------------------------------------------- /src/container/Toast.container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {observer} from 'mobx-react-lite'; 3 | import {StyleSheet, Text, View} from 'react-native'; 4 | import {useStore} from 'Root.store'; 5 | import tw from 'tailwind-rn'; 6 | 7 | export const ToastContainer = observer(() => { 8 | const root = useStore(); 9 | const {toasts} = root.ui; 10 | 11 | if (!toasts.length) { 12 | return null; 13 | } 14 | 15 | const latestToast = toasts[0]; 16 | 17 | const conditionalStyle = styles[latestToast.type]; 18 | 19 | return ( 20 | 26 | {latestToast.text} 27 | 28 | ); 29 | }); 30 | 31 | const styles = StyleSheet.create({ 32 | success: tw(`bg-green-500`), 33 | error: tw(`bg-red-500`), 34 | neutral: { 35 | // @ts-ignore 36 | backgroundColor: { 37 | dynamic: { 38 | dark: global.colors.gray500, 39 | light: `white`, 40 | }, 41 | }, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NodeList.container'; 2 | export * from './GeneralConfig.container'; 3 | export * from './AddToken.container'; 4 | export * from './IgnoreConfig.container'; 5 | export * from './GithubActionsConfig.container'; 6 | export * from './Toast.container'; 7 | -------------------------------------------------------------------------------- /src/lib/CIDemonNative.ts: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | 3 | interface CIDemonNative { 4 | setStatusButtonText: (passed: number, running: number, failed: number, simpleIcon: boolean) => void; 5 | requestReview: () => void; 6 | sendNotification: (title: string, payload: string, url: string) => void; 7 | securelyStore: (key: string, value: string) => void; 8 | securelyRetrieve: (key: string) => string | null; 9 | applyAutoLauncher: (autoLauncherValue: boolean) => void; 10 | closeApp: () => void; 11 | showShareMenu: (x: number, y: number, text: string) => void; 12 | } 13 | 14 | function createCIDemonNative(nativeModule: any): CIDemonNative { 15 | return { 16 | setStatusButtonText: nativeModule.setStatusButtonText, 17 | requestReview: nativeModule.requestReview, 18 | sendNotification: __DEV__ ? () => null : nativeModule.sendNotification, 19 | securelyStore: nativeModule.securelyStore, 20 | securelyRetrieve: nativeModule.securelyRetrieve, 21 | applyAutoLauncher: nativeModule.applyAutoLauncher, 22 | closeApp: nativeModule.closeApp, 23 | showShareMenu: nativeModule.showShareMenu 24 | }; 25 | } 26 | 27 | export const cidemonNative = createCIDemonNative(NativeModules.CIDemonNative); -------------------------------------------------------------------------------- /src/lib/DtoMapper.test.ts: -------------------------------------------------------------------------------- 1 | import {mapCircleCIProjects} from "./DtoMappings" 2 | 3 | describe(`DTO Mapper behaves correctly`, () => { 4 | it(`Handles CircleCI workflows correctly`, () => { 5 | const circleCIResWithFailedJob = [ 6 | { 7 | branches: { 8 | "bet-fix-blob-error-state": { 9 | latest_workflows: { 10 | commit_validation: { 11 | status: `failed`, 12 | created_at: `2020-10-14T15:36:34.176Z`, 13 | id: `113c951a-f60c-4579-8dd8-d3ee988e4ecc`, 14 | }, 15 | }, 16 | pusher_logins: [`bhuang14`], 17 | running_builds: [], 18 | recent_builds: [ 19 | { 20 | outcome: `success`, 21 | status: `success`, 22 | build_num: 460348, 23 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 24 | pushed_at: `2020-10-14T15:36:33.000Z`, 25 | is_workflow_job: true, 26 | is_2_0_job: true, 27 | added_at: `2020-10-14T15:41:32.083Z`, 28 | }, 29 | { 30 | outcome: `failed`, 31 | status: `failed`, 32 | build_num: 460347, 33 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 34 | pushed_at: `2020-10-14T15:36:33.000Z`, 35 | is_workflow_job: true, 36 | is_2_0_job: true, 37 | added_at: `2020-10-14T15:47:20.003Z`, 38 | }, 39 | { 40 | outcome: `success`, 41 | status: `success`, 42 | build_num: 460346, 43 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 44 | pushed_at: `2020-10-14T15:36:33.000Z`, 45 | is_workflow_job: true, 46 | is_2_0_job: true, 47 | added_at: `2020-10-14T15:52:43.128Z`, 48 | }, 49 | { 50 | outcome: `success`, 51 | status: `success`, 52 | build_num: 460345, 53 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 54 | pushed_at: `2020-10-14T15:36:33.000Z`, 55 | is_workflow_job: true, 56 | is_2_0_job: true, 57 | added_at: `2020-10-14T15:41:40.057Z`, 58 | }, 59 | { 60 | outcome: `success`, 61 | status: `success`, 62 | build_num: 460344, 63 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 64 | pushed_at: `2020-10-14T15:36:33.000Z`, 65 | is_workflow_job: true, 66 | is_2_0_job: true, 67 | added_at: `2020-10-14T15:38:18.937Z`, 68 | }, 69 | ], 70 | last_success: { 71 | outcome: `success`, 72 | status: `success`, 73 | build_num: 460348, 74 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 75 | pushed_at: `2020-10-14T15:36:33.000Z`, 76 | is_workflow_job: true, 77 | is_2_0_job: true, 78 | added_at: `2020-10-14T15:41:32.083Z`, 79 | }, 80 | last_non_success: { 81 | outcome: `failed`, 82 | status: `failed`, 83 | build_num: 460347, 84 | vcs_revision: `d7081bf668886ec52f2c5de70dcfa329e7fefa73`, 85 | pushed_at: `2020-10-14T15:36:33.000Z`, 86 | is_workflow_job: true, 87 | is_2_0_job: true, 88 | added_at: `2020-10-14T15:47:20.003Z`, 89 | }, 90 | latest_completed_workflows: { 91 | commit_validation: { 92 | status: `failed`, 93 | created_at: `2020-10-14T15:36:34.176Z`, 94 | id: `113c951a-f60c-4579-8dd8-d3ee988e4ecc`, 95 | }, 96 | }, 97 | is_using_workflows: true, 98 | }, 99 | }, 100 | oss: false, 101 | reponame: `experimental`, 102 | parallel: 11, 103 | username: `kr-project`, 104 | has_usable_key: false, 105 | vcs_type: `github`, 106 | language: null, 107 | vcs_url: `https://github.com/kr-project/experimental`, 108 | following: true, 109 | default_branch: `master`, 110 | }, 111 | ] 112 | 113 | const mappedNodes = mapCircleCIProjects(circleCIResWithFailedJob, `blah`, { 114 | showBuildNumber: true, 115 | }) 116 | 117 | expect(mappedNodes[0].status).toBe(`failed`) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /src/lib/DtoMappings.ts: -------------------------------------------------------------------------------- 1 | import {DateTime} from 'luxon'; 2 | import { 3 | CircleciRepoDto, 4 | AppcenterRepoDto, 5 | AppcenterBranchDto, 6 | TravisRepoDto, 7 | TravisBranchesDto, 8 | BitriseBranchDto, 9 | BitriseRepoDto, 10 | BitriseStatus, 11 | GithubWorkflowDto, 12 | GithubWorkflowRunInfo, 13 | GitlabProjectDto, 14 | GitlabPipelineDto, 15 | IGithubCheck, 16 | } from 'model'; 17 | 18 | interface IParsingOptions { 19 | showBuildNumber: boolean; 20 | } 21 | 22 | export function mapCircleCIProjects( 23 | repos: CircleciRepoDto[], 24 | key: string, 25 | options: IParsingOptions, 26 | ): INode[] { 27 | let nodes: INode[] = []; 28 | 29 | for (let i = 0; i < repos.length; i++) { 30 | let repo = repos[i]; 31 | let branches = Object.entries(repo.branches); 32 | for (let j = 0; j < branches.length; j++) { 33 | let [name, info] = branches[j]; 34 | 35 | let status: Status = `running`; 36 | let isRunning = !!info.running_builds?.length; 37 | let latestDate; 38 | 39 | let jobId: string = `-`; 40 | 41 | if (info.last_success && info.last_non_success) { 42 | if (info.last_success.pushed_at > info.last_non_success.pushed_at) { 43 | status = `passed`; 44 | latestDate = info.last_success.pushed_at; 45 | jobId = info.last_success.build_num.toString(); 46 | } else { 47 | status = `failed`; 48 | latestDate = info.last_non_success.pushed_at; 49 | jobId = info.last_non_success.build_num.toString(); 50 | } 51 | } else if (info.last_success) { 52 | status = `passed`; 53 | latestDate = info.last_success.pushed_at; 54 | jobId = info.last_success.build_num.toString(); 55 | } else if (info.last_non_success) { 56 | status = `failed`; 57 | latestDate = info.last_non_success.pushed_at; 58 | jobId = info.last_non_success.build_num.toString(); 59 | } 60 | 61 | if (isRunning) { 62 | status = `running`; 63 | latestDate = info.running_builds![0].pushed_at; 64 | } 65 | 66 | let vcsLong = repo.vcs_url.includes(`github`) ? `github` : `bitbucket`; 67 | let vcsShort = repo.vcs_url.includes(`github`) ? `gh` : `bb`; 68 | let unscapedName = unescape(name); 69 | 70 | if (!options.showBuildNumber) { 71 | jobId = ``; 72 | } 73 | 74 | let node: INode = { 75 | id: `${`CircleCI`}-${repo.username}-${repo.reponame}-${name}`, 76 | url: `https://circleci.com/${vcsShort}/${repo.username}/${repo.reponame}/tree/${name}`, 77 | date: latestDate, 78 | source: `CircleCI`, 79 | label: `${repo.username}/${repo.reponame} [${unscapedName}] #${jobId}`, 80 | status: status, 81 | key: key, 82 | buildUrl: `https://circleci.com/api/v1.1/project/${vcsLong}/${repo.username}/${repo.reponame}/tree/${name}?circle-token=${key}`, 83 | slug: `${repo.username}/${repo.reponame}`, 84 | }; 85 | 86 | nodes.push(node); 87 | } 88 | } 89 | 90 | return nodes; 91 | } 92 | 93 | export function mapAppcenterTuplesToNodes( 94 | repo: AppcenterRepoDto, 95 | branches: AppcenterBranchDto[], 96 | key: string, 97 | options: IParsingOptions, 98 | ): INode[] { 99 | return branches.map((branch) => { 100 | let status: Status = `pending`; 101 | 102 | if (branch.lastBuild?.status === `inProgress`) { 103 | status = `running`; 104 | } 105 | 106 | if (branch.lastBuild?.result === `succeeded`) { 107 | status = `passed`; 108 | } 109 | 110 | if (branch.lastBuild?.result === `failed`) { 111 | status = `failed`; 112 | } 113 | 114 | let jobId = ``; 115 | if (options.showBuildNumber && branch.lastBuild) { 116 | jobId = ` #${branch.lastBuild.buildNumber}`; 117 | } 118 | 119 | let urlFriendlyBranchName = branch.branch.name.replace(`/`, `%2F`); 120 | 121 | let node: INode = { 122 | id: `${`AppCenter`}-${repo.owner.name}-${repo.name}-${ 123 | branch.branch.name 124 | }`, 125 | url: `https://appcenter.ms/users/${repo.owner.name}/apps/${repo.name}/build/branches/${urlFriendlyBranchName}`, 126 | label: `${repo.owner.name}/${repo.name} [${branch.branch.name}]${jobId}`, 127 | source: `AppCenter`, 128 | status: status, 129 | key: key, 130 | buildUrl: `https://api.appcenter.ms/v0.1/apps/${repo.owner.name}/${repo.name}/branches/${urlFriendlyBranchName}/builds`, 131 | slug: `${repo.owner.name}/${repo.name}`, 132 | }; 133 | return node; 134 | }); 135 | } 136 | 137 | export function mapTravisTuplesToNodes( 138 | repo: TravisRepoDto, 139 | branches: TravisBranchesDto, 140 | key: string, 141 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 142 | options: IParsingOptions, 143 | ): INode[] { 144 | return branches.branches.map((branch) => { 145 | let commit = branches.commits.find((c) => c.id === branch.commit_id)!; 146 | 147 | let status: Status = `pending`; 148 | if (branch.state === `errored`) { 149 | status = `failed`; 150 | } 151 | 152 | if (branch.state === `started`) { 153 | status = `running`; 154 | } 155 | 156 | if (branch.state === `passed`) { 157 | status = `passed`; 158 | } 159 | 160 | let node: INode = { 161 | id: `${`TravisCI`}-${repo.slug}-${commit.branch}`, 162 | url: `https://travis-ci.com/github/${repo.slug}`, 163 | label: `${repo.slug} [${commit.branch}]`, 164 | source: `TravisCI`, 165 | status: status, 166 | key: key, 167 | date: branch.started_at, 168 | slug: repo.slug, 169 | }; 170 | 171 | return node; 172 | }); 173 | } 174 | 175 | export function mapBitriseTuplesToNode( 176 | repo: BitriseRepoDto, 177 | branches: BitriseBranchDto[], 178 | key: string, 179 | options: IParsingOptions, 180 | ): INode[] { 181 | return branches.map((branch) => { 182 | let status: Status = `pending`; 183 | 184 | switch (branch.status) { 185 | case BitriseStatus.successful: 186 | status = `passed`; 187 | break; 188 | case BitriseStatus.abortedWithFailure: 189 | status = `failed`; 190 | break; 191 | case BitriseStatus.abortedWithSuccess: 192 | status = `passed`; 193 | break; 194 | case BitriseStatus.notFinished: 195 | status = `running`; 196 | break; 197 | case BitriseStatus.failed: 198 | status = `failed`; 199 | break; 200 | default: 201 | status = `pending`; 202 | break; 203 | } 204 | 205 | let jobId = ``; 206 | if (options.showBuildNumber) { 207 | jobId = ` #${branch.build_number}`; 208 | } 209 | 210 | let mainName = branch.branch; 211 | 212 | if (!mainName || mainName.length === 0) { 213 | mainName = branch.commit_message ?? 'Unknown Branch'; 214 | } 215 | 216 | if (branch.triggered_workflow) { 217 | mainName += `- ${branch.triggered_workflow}`; 218 | } 219 | 220 | let node: INode = { 221 | id: `${`Bitrise`}-${repo.title}-${branch.branch}-${branch.build_number}`, 222 | url: `https://app.bitrise.io/build/${branch.slug}`, 223 | label: `${repo.repo_owner}/${repo.title} [${mainName}]${jobId}`, 224 | source: `Bitrise`, 225 | status, 226 | key, 227 | // @TODO do not only use finished_at, for running builds use created_at 228 | date: branch.finished_at, 229 | buildUrl: `https://api.bitrise.io/v0.1/apps/${repo.slug}/builds`, 230 | // Storing the API provided repository "title" to be used in further calls to the api 231 | extra: repo.title, 232 | vcs: 'unknown', 233 | jobId: branch.build_number?.toString(), 234 | slug: `${repo.repo_owner}/${repo.title}`, 235 | }; 236 | 237 | return node; 238 | }); 239 | } 240 | 241 | export function mapGithubActionTupleToNode( 242 | slug: string, 243 | workflowDto: GithubWorkflowDto, 244 | latestRun: GithubWorkflowRunInfo, 245 | key: string, 246 | options: IParsingOptions, 247 | ): INode { 248 | let status: Status = `pending`; 249 | 250 | switch (latestRun.conclusion) { 251 | case `success`: 252 | status = `passed`; 253 | break; 254 | 255 | case `failure`: 256 | status = `failed`; 257 | break; 258 | 259 | case `in_progress`: 260 | status = `running`; 261 | break; 262 | 263 | default: 264 | break; 265 | } 266 | 267 | let pathComponent = `workflow%3A${workflowDto.name}`; 268 | 269 | let runNumber = ``; 270 | if (options.showBuildNumber) { 271 | runNumber = ` #${latestRun.run_number}`; 272 | } 273 | 274 | let node: INode = { 275 | id: `${`Github`}-${slug}-${workflowDto.name}`, 276 | url: `https://github.com/${slug}/actions?query=${pathComponent}`, 277 | label: `${slug} [${workflowDto.name}]${runNumber}`, 278 | date: latestRun.created_at, 279 | status: status, 280 | jobId: `${latestRun.run_number}`, 281 | source: `Github`, 282 | vcs: `github`, 283 | key: key, 284 | slug, 285 | }; 286 | 287 | return node; 288 | } 289 | 290 | export function mapGitlabTupleToNodes( 291 | project: GitlabProjectDto, 292 | pipelines: GitlabPipelineDto[], 293 | key: string, 294 | options: IParsingOptions, 295 | ): INode[] { 296 | // A single ref can have multiple entries on the pipeline, which makes the output confusing 297 | // filter the repeated entries, just take the first one that appears 298 | return pipelines 299 | .reduce( 300 | (acc: GitlabPipelineDto[], a: GitlabPipelineDto): GitlabPipelineDto[] => { 301 | if (acc.findIndex((b) => b.ref === a.ref) === -1) { 302 | acc.push(a); 303 | } 304 | 305 | return acc; 306 | }, 307 | [] as any, 308 | ) 309 | .map((pipeline) => { 310 | let status: Status = `pending`; 311 | 312 | switch (pipeline.status) { 313 | case `failed`: 314 | status = `failed`; 315 | break; 316 | case `running`: 317 | status = `running`; 318 | break; 319 | case `success`: 320 | status = `passed`; 321 | break; 322 | case `canceled`: // not a typo, gitlab is wrong 323 | status = `pending`; 324 | break; 325 | default: 326 | status = `pending`; 327 | break; 328 | } 329 | 330 | let pipelineId = ``; 331 | if (options.showBuildNumber) { 332 | pipelineId = ` #${pipeline.id}`; 333 | } 334 | 335 | let node: INode = { 336 | id: `${`Gitlab`}-${project.name}-${pipeline.id}`, 337 | url: pipeline.web_url, 338 | label: `${project.name} [${pipeline.ref}] ${pipelineId}`, 339 | date: pipeline.created_at, 340 | status: status, 341 | jobId: pipeline.id, 342 | source: `Gitlab`, 343 | vcs: `gitlab`, 344 | key: key, 345 | slug: project.name, 346 | }; 347 | 348 | return node; 349 | }); 350 | } 351 | 352 | export function mapGithubPrToNode( 353 | slug: string, 354 | pr: any, 355 | statuses: IGithubCheck[], 356 | key: string, 357 | ): INode { 358 | let status: Status = `pending`; 359 | let subItems: ISubNode[] = []; 360 | 361 | if (statuses.length) { 362 | status = `passed`; 363 | 364 | statuses.forEach( 365 | ({name, conclusion, started_at, completed_at, details_url}) => { 366 | let lStartedAt = DateTime.fromISO(started_at); 367 | 368 | let subItem: ISubNode = { 369 | label: name, 370 | status: `pending`, 371 | url: details_url, 372 | }; 373 | 374 | if (!conclusion) { 375 | if (status !== `failed`) { 376 | status = `running`; 377 | } 378 | 379 | let extraLabel = lStartedAt.toRelative({unit: 'minutes'}); 380 | subItem.extraLabel = extraLabel; 381 | } else { 382 | let lCompletedAt = DateTime.fromISO(completed_at!); 383 | let extraLabel = `${Math.round( 384 | lCompletedAt.diff(lStartedAt, 'minutes').minutes, 385 | )}m`; 386 | subItem.extraLabel = extraLabel; 387 | 388 | if (conclusion === `failure` || conclusion === `timed_out`) { 389 | status = `failed`; 390 | subItem.status = `failed`; 391 | } else if (conclusion === `success`) { 392 | subItem.status = `passed`; 393 | } else if (conclusion === `neutral`) { 394 | subItem.status = `pending`; 395 | } 396 | } 397 | 398 | subItems.push(subItem); 399 | }, 400 | ); 401 | } 402 | 403 | let node: INode = { 404 | id: pr.id, 405 | url: pr.html_url, 406 | label: `${slug} [${pr.title}]`, 407 | date: pr.updated_at, 408 | status: status, 409 | jobId: pr.id, 410 | source: `Github`, 411 | vcs: `github`, 412 | key: key, 413 | subItems: subItems, 414 | sha: pr.head.sha, 415 | isPr: true, 416 | username: pr.user.login, 417 | userAvatarUrl: pr.user.avatar_url, 418 | slug, 419 | }; 420 | 421 | return node; 422 | } 423 | 424 | export function mapGithubBranchToNode( 425 | slug: string, 426 | branch: any, 427 | statuses: IGithubCheck[], 428 | key: string, 429 | ): INode { 430 | let status: Status = `pending`; 431 | let subItems: ISubNode[] = []; 432 | 433 | if (statuses.length) { 434 | status = `passed`; 435 | 436 | statuses.forEach( 437 | ({name, conclusion, started_at, completed_at, details_url}) => { 438 | let lStartedAt = DateTime.fromISO(started_at); 439 | 440 | let subItem: ISubNode = { 441 | label: name, 442 | status: `pending`, 443 | url: details_url, 444 | }; 445 | 446 | if (!conclusion) { 447 | if (status !== `failed`) { 448 | status = `running`; 449 | } 450 | 451 | let extraLabel = lStartedAt.toRelative({unit: 'minutes'}); 452 | subItem.extraLabel = extraLabel; 453 | } else { 454 | let lCompletedAt = DateTime.fromISO(completed_at!); 455 | let extraLabel = `${Math.round( 456 | lCompletedAt.diff(lStartedAt, 'minutes').minutes, 457 | )}m`; 458 | subItem.extraLabel = extraLabel; 459 | 460 | if (conclusion === `failure` || conclusion === `timed_out`) { 461 | status = `failed`; 462 | subItem.status = `failed`; 463 | } else if (conclusion === `success`) { 464 | subItem.status = `passed`; 465 | } else if (conclusion === `neutral`) { 466 | subItem.status = `pending`; 467 | } 468 | } 469 | 470 | subItems.push(subItem); 471 | }, 472 | ); 473 | } 474 | 475 | let node: INode = { 476 | id: branch.commit.sha, 477 | url: `https://github.com/${slug}/commit/${branch.commit.sha}`, 478 | label: `${slug} [${branch.name}]`, 479 | // date: pr.updated_at, 480 | status: status, 481 | // jobId: pr.id, 482 | source: `Github`, 483 | vcs: `github`, 484 | key: key, 485 | subItems: subItems, 486 | sha: branch.commit.sha, 487 | slug, 488 | isBranch: true, 489 | }; 490 | 491 | return node; 492 | } 493 | 494 | export function mapGithubActionRunToNode( 495 | slug: string, 496 | run: any, 497 | key: string, 498 | ): INode { 499 | let status: Status = `pending`; 500 | 501 | if (run.conclusion === `failure` || run.conclusion === `timed_out`) { 502 | status = `failed`; 503 | } else if (run.conclusion === `success`) { 504 | status = `passed`; 505 | } else if (run.conclusion === `neutral`) { 506 | status = `pending`; 507 | } 508 | 509 | let node: INode = { 510 | id: run.id, 511 | url: `https://github.com/${slug}/actions/runs/${run.id}`, 512 | label: `${slug} [${run.name}]`, 513 | date: run.updated_at, 514 | status: status, 515 | source: `Github`, 516 | vcs: `github`, 517 | key: key, 518 | sha: run.head_sha, 519 | isAction: true, 520 | slug, 521 | }; 522 | 523 | return node; 524 | } 525 | -------------------------------------------------------------------------------- /src/lib/__mocks__/CIDemonNative.ts: -------------------------------------------------------------------------------- 1 | interface CIDemonNative { 2 | requestReview: () => void; 3 | sendNotification: (title: string, payload: string, url: string) => void; 4 | securelyStore: (key: string, value: string) => void; 5 | securelyRetrieve: (key: string) => string | null; 6 | applyAutoLauncher: (autoLauncherValue: boolean) => void; 7 | closeApp: () => void; 8 | } 9 | 10 | function createCIDemonNative(): CIDemonNative { 11 | return { 12 | requestReview: jest.fn(), 13 | sendNotification: jest.fn(), 14 | securelyStore: jest.fn(), 15 | securelyRetrieve: jest.fn(), 16 | applyAutoLauncher: jest.fn(), 17 | closeApp: jest.fn(), 18 | }; 19 | } 20 | 21 | export const cidemonNative = createCIDemonNative(); 22 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export let TINT_MAPPING: Record = { 2 | passed: `#65ea92`, 3 | failed: `#eb4e49`, 4 | running: `#FFBE30`, 5 | pending: {dynamic: {light: `#37474F`, dark: `#DDD`}}, 6 | }; 7 | export let REGEX_VALID_URL = new RegExp( 8 | '^' + 9 | // protocol identifier 10 | '(?:(?:https?|ftp)://)' + 11 | // user:pass authentication 12 | '(?:\\S+(?::\\S*)?@)?' + 13 | '(?:' + 14 | // IP address exclusion 15 | // private & local networks 16 | '(?!(?:10|127)(?:\\.\\d{1,3}){3})' + 17 | '(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' + 18 | '(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' + 19 | // IP address dotted notation octets 20 | // excludes loopback network 0.0.0.0 21 | // excludes reserved space >= 224.0.0.0 22 | // excludes network & broacast addresses 23 | // (first & last IP address of each class) 24 | '(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' + 25 | '(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' + 26 | '(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' + 27 | '|' + 28 | // host name 29 | '(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' + 30 | // domain name 31 | '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' + 32 | // TLD identifier 33 | '(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' + 34 | // TLD may end with dot 35 | '\\.?' + 36 | ')' + 37 | // port number 38 | '(?::\\d{2,5})?' + 39 | // resource path 40 | '(?:[/?#]\\S*)?' + 41 | '$', 42 | 'i', 43 | ); 44 | -------------------------------------------------------------------------------- /src/lib/generalUtils.ts: -------------------------------------------------------------------------------- 1 | export function idExtractor(i: T) { 2 | return i.id.toString(); 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | import {useColorScheme} from 'react-native'; 3 | 4 | export function useDarkTheme() { 5 | let theme = useColorScheme(); 6 | 7 | return theme === `dark`; 8 | } 9 | 10 | export function useDynamic() { 11 | let theme = useColorScheme(); 12 | 13 | return (darkThemeValue: string, lightThemeValue: string) => 14 | theme === 'dark' ? darkThemeValue : lightThemeValue; 15 | } 16 | 17 | export function useBoolean( 18 | initialState: boolean = false, 19 | ): [boolean, () => void, () => void] { 20 | let [v, setV] = useState(initialState); 21 | let setTrue = () => { 22 | setV(true); 23 | }; 24 | let setFalse = () => { 25 | setV(false); 26 | }; 27 | return [v, setTrue, setFalse]; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CIDemonNative'; 2 | export * from './DtoMappings'; 3 | export * from './hooks'; 4 | export * from './constants'; 5 | export * from './generalUtils'; 6 | -------------------------------------------------------------------------------- /src/model/dto/AppcenterBranch.dto.ts: -------------------------------------------------------------------------------- 1 | export interface AppcenterBranchDto { 2 | branch: BranchInfoDto 3 | lastBuild: BuildInfoDto 4 | configured: boolean 5 | } 6 | 7 | interface BranchInfoDto { 8 | name: string 9 | } 10 | 11 | interface BuildInfoDto { 12 | id: number 13 | buildNumber: string 14 | sourceVersion: string 15 | status: string // "inProgress" 16 | result: string // "succeeded" / "failed" / among many others 17 | } 18 | -------------------------------------------------------------------------------- /src/model/dto/AppcenterRepo.dto.ts: -------------------------------------------------------------------------------- 1 | export interface AppcenterRepoDto { 2 | id: string; 3 | name: string; 4 | owner: OwnerDto; 5 | } 6 | 7 | interface OwnerDto { 8 | id: string; 9 | name: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/model/dto/Bitrise.dto.ts: -------------------------------------------------------------------------------- 1 | export interface BitrisePaginated { 2 | data: T[] 3 | } 4 | 5 | export interface BitriseRepoDto { 6 | slug: string 7 | title: string 8 | repo_owner: string 9 | provider: string 10 | branch: string 11 | } 12 | 13 | export interface BitriseBranchDto { 14 | status: BitriseStatus 15 | build_number: number 16 | commit_message?: string 17 | slug: string 18 | branch: string 19 | finished_at?: string 20 | triggered_workflow?: string 21 | } 22 | 23 | export enum BitriseStatus { 24 | notFinished = 0, 25 | successful = 1, 26 | failed = 2, 27 | abortedWithFailure = 3, 28 | abortedWithSuccess = 4, 29 | } 30 | -------------------------------------------------------------------------------- /src/model/dto/CircleciRepo.dto.ts: -------------------------------------------------------------------------------- 1 | export interface CircleciRepoDto { 2 | vcs_url: string 3 | username: string 4 | reponame: string 5 | branches: Record 6 | } 7 | 8 | interface BranchDto { 9 | recent_builds?: BuildInfoDto[] 10 | running_builds?: BuildInfoDto[] 11 | last_non_success?: BuildInfoDto 12 | last_success: BuildInfoDto 13 | latest_completed_workflows: Record 14 | is_using_workflows: boolean 15 | } 16 | 17 | interface BuildInfoDto { 18 | pushed_at: string 19 | vcs_revision: string 20 | build_num: number 21 | outcome: string 22 | } 23 | 24 | interface WorkflowDto { 25 | status: string 26 | created_at: string 27 | id: string 28 | } 29 | -------------------------------------------------------------------------------- /src/model/dto/Github.dto.ts: -------------------------------------------------------------------------------- 1 | export interface GithubWorkflowsDto { 2 | total_count: number; 3 | workflows: GithubWorkflowDto[]; 4 | } 5 | 6 | export interface GithubWorkflowDto { 7 | id: number; 8 | name: string; 9 | url: string; 10 | } 11 | 12 | export interface GithubWorkflowInfoDto { 13 | total_count: number; 14 | workflow_runs: GithubWorkflowRunInfo[]; 15 | } 16 | 17 | export interface GithubWorkflowRunInfo { 18 | id: number; 19 | run_number: number; 20 | status: string; 21 | conclusion: string; 22 | created_at: string; 23 | } 24 | 25 | export interface IGithubCheck { 26 | id: number; 27 | name: string; 28 | node_id: string; 29 | head_sha: string; 30 | external_id: string; 31 | url: string; 32 | html_url: string; 33 | details_url: string; 34 | status: string; 35 | conclusion?: string; 36 | started_at: string; 37 | completed_at?: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/model/dto/Gitlab.dto.ts: -------------------------------------------------------------------------------- 1 | export interface GitlabProjectDto { 2 | id: string 3 | name: string 4 | web_url: string 5 | } 6 | 7 | export interface GitlabPipelineDto { 8 | id: string 9 | ref: string 10 | status: string 11 | created_at: string 12 | web_url: string 13 | } 14 | -------------------------------------------------------------------------------- /src/model/dto/TravisBranches.dto.ts: -------------------------------------------------------------------------------- 1 | export interface TravisBranchesDto { 2 | branches: BranchDto[]; 3 | commits: CommitDto[]; 4 | } 5 | 6 | interface BranchDto { 7 | id: number; 8 | commit_id: number; 9 | state: string; 10 | started_at: string; 11 | } 12 | 13 | interface CommitDto { 14 | id: number; 15 | branch: string; 16 | message: string; 17 | author_name: string; 18 | author_email: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/model/dto/TravisRepos.dto.ts: -------------------------------------------------------------------------------- 1 | export interface TravisReposDto { 2 | repos: TravisRepoDto[]; 3 | } 4 | 5 | export interface TravisRepoDto { 6 | id: number; 7 | slug: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dto/AppcenterBranch.dto'; 2 | export * from './dto/AppcenterRepo.dto'; 3 | export * from './dto/Bitrise.dto'; 4 | export * from './dto/CircleciRepo.dto'; 5 | export * from './dto/Github.dto'; 6 | export * from './dto/Gitlab.dto'; 7 | export * from './dto/TravisBranches.dto'; 8 | export * from './dto/TravisRepos.dto'; 9 | -------------------------------------------------------------------------------- /src/store/Api.store.ts: -------------------------------------------------------------------------------- 1 | import {IRootStore} from 'Root.store'; 2 | import axios from 'axios'; 3 | import { 4 | mapAppcenterTuplesToNodes, 5 | mapBitriseTuplesToNode, 6 | mapCircleCIProjects, 7 | mapGithubPrToNode, 8 | mapGithubBranchToNode, 9 | mapGitlabTupleToNodes, 10 | mapTravisTuplesToNodes, 11 | mapGithubActionRunToNode, 12 | } from 'lib'; 13 | import { 14 | AppcenterRepoDto, 15 | TravisReposDto, 16 | BitrisePaginated, 17 | BitriseRepoDto, 18 | BitriseBranchDto, 19 | GitlabProjectDto, 20 | GitlabPipelineDto, 21 | CircleciRepoDto, 22 | } from 'model'; 23 | import Github from 'github-api'; 24 | import allSettled from 'promise.allsettled'; 25 | 26 | interface AjaxCallParams { 27 | auth?: string; 28 | method: `GET` | `POST` | `DELETE` | `PUT`; 29 | url: string; 30 | body?: any; 31 | headers?: any; 32 | } 33 | 34 | interface IFetchOptions { 35 | showBuildNumber: boolean; 36 | } 37 | 38 | export let createApiStore = (root: IRootStore) => { 39 | let ajaxCall = async ({ 40 | url, 41 | auth, 42 | method, 43 | body, 44 | headers = {}, 45 | }: AjaxCallParams) => { 46 | let res = await axios({ 47 | url, 48 | method, 49 | headers: { 50 | Authorization: auth, 51 | 'Content-Type': `application/json`, 52 | ...headers, 53 | }, 54 | data: body ? JSON.stringify(body) : undefined, 55 | }); 56 | 57 | return res.data; 58 | }; 59 | 60 | let get = (props: { 61 | url: string; 62 | auth?: string; 63 | headers?: Record; 64 | }) => ajaxCall({method: `GET`, ...props}); 65 | 66 | let post = (props: { 67 | url: string; 68 | auth?: string; 69 | body?: any; 70 | headers?: Record; 71 | }) => ajaxCall({method: `POST`, ...props}); 72 | 73 | // let put = (props: { 74 | // url: string 75 | // auth?: string 76 | // headers?: Record 77 | // }) => ajaxCall({method: `PUT`, ...props}) 78 | 79 | let store = { 80 | // _ _ 81 | // /\ | | (_) 82 | // / \ ___| |_ _ ___ _ __ ___ 83 | // / /\ \ / __| __| |/ _ \| '_ \/ __| 84 | // / ____ \ (__| |_| | (_) | | | \__ \ 85 | // /_/ \_\___|\__|_|\___/|_| |_|___/ 86 | fetchCircleciNodes: async ( 87 | token: string, 88 | options: IFetchOptions, 89 | ): Promise => { 90 | try { 91 | let repos: CircleciRepoDto[] = await get({ 92 | url: `https://circleci.com/api/v1.1/projects?circle-token=${token}`, 93 | }); 94 | return mapCircleCIProjects(repos, token, options); 95 | } catch (e) { 96 | root.ui.addToast({ 97 | text: 'Could not fetch CircleCI builds', 98 | type: 'error', 99 | }); 100 | return []; 101 | } 102 | }, 103 | 104 | fetchAppcenterNodes: async ( 105 | key: string, 106 | options: IFetchOptions, 107 | ): Promise => { 108 | try { 109 | let repos: AppcenterRepoDto[] = await get({ 110 | url: `https://api.appcenter.ms/v0.1/apps`, 111 | headers: { 112 | 'X-API-Token': key, 113 | }, 114 | }); 115 | 116 | let branchPromises = repos.map((r) => 117 | get({ 118 | url: `https://api.appcenter.ms/v0.1/apps/${r.owner.name}/${r.name}/branches`, 119 | headers: { 120 | 'X-API-Token': key, 121 | }, 122 | }), 123 | ); 124 | 125 | let resolvedBranches = await allSettled(branchPromises); 126 | 127 | let nodes = resolvedBranches 128 | .map((branchesResult, ii) => { 129 | if (branchesResult.status === 'fulfilled') { 130 | return mapAppcenterTuplesToNodes( 131 | repos[ii], 132 | branchesResult.value, 133 | key, 134 | options, 135 | ); 136 | } else { 137 | return []; 138 | } 139 | }) 140 | .flat(); 141 | 142 | return nodes; 143 | } catch (e) { 144 | root.ui.addToast({ 145 | text: 'Could not fetch App Center builds', 146 | type: 'error', 147 | }); 148 | return []; 149 | } 150 | }, 151 | 152 | fetchTravisciNodes: async ( 153 | key: string, 154 | options: IFetchOptions, 155 | ): Promise => { 156 | try { 157 | let reposDto: TravisReposDto = await get({ 158 | url: `https://api.travis-ci.com/repos`, 159 | headers: { 160 | Accept: `application/vnd.travis-ci.2.1+json`, 161 | Authorization: `token ${key}`, 162 | 'User-Agent': `Tempomat/2.0.0`, 163 | }, 164 | }); 165 | 166 | let branchPromises = reposDto.repos.map((r) => 167 | get({ 168 | url: `https://api.travis-ci.com/repos/${r.slug}/branches`, 169 | headers: { 170 | Accept: `application/vnd.travis-ci.2.1+json`, 171 | Authorization: `token ${key}`, 172 | 'User-Agent': `Tempomat/2.0.0`, 173 | }, 174 | }), 175 | ); 176 | 177 | let resolvedBranches = await allSettled(branchPromises); 178 | 179 | return resolvedBranches 180 | .map((branchesRes, ii) => { 181 | if (branchesRes.status === 'fulfilled') { 182 | return mapTravisTuplesToNodes( 183 | reposDto.repos[ii], 184 | branchesRes.value, 185 | key, 186 | options, 187 | ); 188 | } else { 189 | return []; 190 | } 191 | }) 192 | .flat(); 193 | } catch (e) { 194 | root.ui.addToast({ 195 | text: 'Could not fetch CircleCI Nodes', 196 | type: 'error', 197 | }); 198 | return []; 199 | } 200 | }, 201 | 202 | fetchBitriseNodes: async ( 203 | key: string, 204 | options: IFetchOptions, 205 | ): Promise => { 206 | try { 207 | let apps: BitrisePaginated = await get({ 208 | url: `https://api.bitrise.io/v0.1/apps`, 209 | headers: { 210 | Authorization: key, 211 | }, 212 | }); 213 | 214 | let aWeekAgo = new Date(); 215 | aWeekAgo.setDate(aWeekAgo.getDate() - 7); 216 | 217 | let buildsPromises = apps.data.map((app) => 218 | get({ 219 | url: `https://api.bitrise.io/v0.1/apps/${app.slug}/builds`, 220 | headers: { 221 | Authorization: key, 222 | }, 223 | }), 224 | ); 225 | 226 | let builds = await Promise.all(buildsPromises); 227 | 228 | let nodes = builds 229 | .map((bSet, idx) => { 230 | let visitedBranches = new Set(); 231 | 232 | let finalBuilds = bSet.data.filter((b: any) => { 233 | if (visitedBranches.has(b.triggered_workflow)) { 234 | return false; 235 | } else { 236 | visitedBranches.add(b.triggered_workflow); 237 | return true; 238 | } 239 | }); 240 | 241 | return mapBitriseTuplesToNode( 242 | apps.data[idx], 243 | finalBuilds, 244 | key, 245 | options, 246 | ); 247 | }) 248 | .flat(); 249 | 250 | return nodes; 251 | } catch (e) { 252 | root.ui.addToast({ 253 | text: 'Could not fetch Bitrise Nodes', 254 | type: 'error', 255 | }); 256 | return []; 257 | } 258 | }, 259 | 260 | fetchBitriseRepoBranches: async ( 261 | repo: BitriseRepoDto, 262 | branchNames: string[], 263 | key: string, 264 | options: IFetchOptions, 265 | ): Promise => { 266 | try { 267 | let branchesPromises = branchNames.map((name) => { 268 | let encodedName = name.replace(`#`, `%23`).replace(`&`, `%26`); 269 | 270 | return get({ 271 | url: `https://api.bitrise.io/v0.1/apps/${repo.slug}/builds?branch=${encodedName}&limit=1`, 272 | headers: { 273 | Authorization: key, 274 | }, 275 | }); 276 | }); 277 | 278 | let resolvedBranches: Array< 279 | BitrisePaginated 280 | > = await Promise.all(branchesPromises); 281 | 282 | return resolvedBranches 283 | .map((b) => mapBitriseTuplesToNode(repo, b.data, key, options)) 284 | .flat(); 285 | } catch (e) { 286 | root.ui.addToast({ 287 | text: `Error fetching builds for Bitrise branches: ${e}`, 288 | type: 'error', 289 | }); 290 | 291 | return []; 292 | } 293 | }, 294 | 295 | fetchGithubNodes: async ({ 296 | key, 297 | slug, 298 | fetchPrs, 299 | fetchBranches, 300 | fetchWorkflows, 301 | }: { 302 | key: string; 303 | slug: string; 304 | fetchPrs: boolean; 305 | fetchBranches: boolean; 306 | fetchWorkflows: boolean; 307 | }): Promise => { 308 | try { 309 | const githubApi = new Github({ 310 | token: key, 311 | }); 312 | 313 | const [username, reponame] = slug.split(`/`); 314 | 315 | const repo = githubApi.getRepo(username, reponame); 316 | 317 | let nodes: INode[] = []; 318 | 319 | if (fetchPrs) { 320 | const pullRequestsRes = await repo.listPullRequests(); 321 | 322 | const pullRequests: any[] = pullRequestsRes.data; 323 | 324 | const checkPromises = pullRequests.map((pr: any) => 325 | get({ 326 | url: `https://api.github.com/repos/${slug}/commits/${pr.head.sha}/check-runs`, 327 | headers: { 328 | Accept: `application/vnd.github.v3+json`, 329 | Authorization: `token ${key}`, 330 | }, 331 | }), 332 | ); 333 | 334 | const statusesMatrix = await allSettled(checkPromises); 335 | 336 | const prNodes = pullRequests.map((pr: any, index: number) => { 337 | let statusRes = statusesMatrix[index]; 338 | 339 | let checks = 340 | statusRes.status === 'fulfilled' 341 | ? statusRes.value.check_runs ?? [] 342 | : []; 343 | 344 | return mapGithubPrToNode(slug, pr, checks, key); 345 | }); 346 | 347 | nodes.push(...prNodes); 348 | } 349 | 350 | if (fetchBranches) { 351 | const branches = await get({ 352 | url: `https://api.github.com/repos/${slug}/branches?per_page=100`, 353 | headers: { 354 | Accept: `application/vnd.github.v3+json`, 355 | Authorization: `token ${key}`, 356 | }, 357 | }); 358 | 359 | const checkPromises = branches.map((branch: any) => 360 | get({ 361 | url: `https://api.github.com/repos/${slug}/commits/${branch.commit.sha}/check-runs`, 362 | headers: { 363 | Accept: `application/vnd.github.v3+json`, 364 | Authorization: `token ${key}`, 365 | }, 366 | }), 367 | ); 368 | 369 | const statusesMatrix = await allSettled(checkPromises); 370 | 371 | const branchNodes = branches.map((branch: any, index: number) => { 372 | let statusRes = statusesMatrix[index]; 373 | 374 | let checks = 375 | statusRes.status === 'fulfilled' 376 | ? statusRes.value.check_runs ?? [] 377 | : []; 378 | 379 | return mapGithubBranchToNode(slug, branch, checks, key); 380 | }); 381 | 382 | nodes.push(...branchNodes); 383 | } 384 | 385 | if (fetchWorkflows) { 386 | const runsRes = await get({ 387 | url: `https://api.github.com/repos/${slug}/actions/runs?per_page=100`, 388 | headers: { 389 | Accept: `application/vnd.github.v3+json`, 390 | Authorization: `token ${key}`, 391 | }, 392 | }); 393 | 394 | const visitedRuns: Record = {}; 395 | const runs = runsRes.workflow_runs.reduce((acc: any[], run: any) => { 396 | if (!visitedRuns[run.workflow_id]) { 397 | visitedRuns[run.workflow_id] = true; 398 | acc.push(run); 399 | } 400 | 401 | return acc; 402 | }, []); 403 | 404 | const runsNodes: any[] = runs.map((run: any) => 405 | mapGithubActionRunToNode(slug, run, key), 406 | ); 407 | 408 | nodes.push(...runsNodes.flat()); 409 | } 410 | 411 | return nodes; 412 | } catch (e) { 413 | console.warn(`Github error`, e); 414 | root.ui.addToast({ 415 | text: `Could not fetch info for Github repo: ${slug}`, 416 | type: 'error', 417 | }); 418 | return []; 419 | } 420 | }, 421 | 422 | triggerCircleciRebuild: (node: INode) => { 423 | if (!node.buildUrl) { 424 | throw new Error(`Could not rebuild node without a known build url`); 425 | } 426 | 427 | return post({ 428 | url: node.buildUrl, 429 | headers: { 430 | 'Content-Type': `application/json`, 431 | }, 432 | }); 433 | }, 434 | 435 | triggerAppcenterRebuild: async (node: INode) => { 436 | if (!node.buildUrl) { 437 | throw new Error(`Cannot retrigger build without build url`); 438 | } 439 | 440 | return post({ 441 | url: node.buildUrl, 442 | headers: { 443 | 'Content-Type': `application/json`, 444 | 'X-API-Token': node.key!, 445 | }, 446 | }); 447 | }, 448 | 449 | triggerTravisciRebuild: async (_: INode) => { 450 | return Promise.reject( 451 | new Error( 452 | `There is currently no way to manually trigger a job on travisCI`, 453 | ), 454 | ); 455 | }, 456 | 457 | triggerBitriseRebuild: async (node: INode) => { 458 | if (!node.buildUrl) { 459 | throw new Error(`Cannot retrigger build without build url`); 460 | } 461 | 462 | return post({ 463 | url: node.buildUrl, 464 | headers: { 465 | 'Content-Type': `application/json`, 466 | Authorization: node.key!, 467 | }, 468 | body: { 469 | hook_info: { 470 | type: `bitrise`, 471 | }, 472 | build_params: { 473 | branch: node.extra, 474 | }, 475 | }, 476 | }); 477 | }, 478 | 479 | fetchGitlabNodes: async ( 480 | baseURL: string, 481 | visibility: GitlabVisibility, 482 | key: string, 483 | options: IFetchOptions, 484 | ): Promise => { 485 | let nodes: INode[] = []; 486 | 487 | try { 488 | let shouldFetchProjects = true; 489 | let projects: GitlabProjectDto[] = []; 490 | let idAfter: string | null = null; 491 | 492 | while (shouldFetchProjects) { 493 | let url = `${baseURL}/api/v4/projects?${visibility ? `visibility=${visibility}&` : ``}pagination=keyset&simple=true&per_page=100&order_by=id&sort=asc`; 494 | if (!!idAfter) { 495 | url += `&id_after=${idAfter}`; 496 | } 497 | 498 | let projectBatch = await get({ 499 | url, 500 | headers: { 501 | Authorization: `Bearer ${key}`, 502 | }, 503 | }); 504 | if (projectBatch.length === 0) { 505 | shouldFetchProjects = false; 506 | } else { 507 | projects.push(...projectBatch); 508 | idAfter = projectBatch[projectBatch.length - 1].id; 509 | } 510 | } 511 | 512 | let pipelines: Promise[] = projects.map((p) => 513 | get({ 514 | url: `${baseURL}/api/v4/projects/${p.id}/pipelines?per_page=100`, 515 | headers: { 516 | Authorization: `Bearer ${key}`, 517 | }, 518 | }), 519 | ); 520 | 521 | let resolvedPipelines: PromiseSettledResult< 522 | GitlabPipelineDto[] 523 | >[] = await allSettled(pipelines); 524 | 525 | nodes = resolvedPipelines 526 | .map((projectPipelines, ii) => { 527 | if (projectPipelines.status === 'fulfilled') { 528 | return mapGitlabTupleToNodes( 529 | projects[ii], 530 | projectPipelines.value, 531 | key, 532 | options, 533 | ); 534 | } else { 535 | return []; 536 | } 537 | }) 538 | .flat(); 539 | } catch (e) { 540 | root.ui.addToast({ 541 | text: `Could not fetch GitLab repos:\n${e}`, 542 | type: `error`, 543 | }); 544 | } 545 | 546 | return nodes; 547 | }, 548 | }; 549 | 550 | return store; 551 | }; 552 | 553 | export type IApiStore = ReturnType; 554 | -------------------------------------------------------------------------------- /src/store/Node.store.test.ts: -------------------------------------------------------------------------------- 1 | import {createNodeStore, SortingKey} from './Node.store'; 2 | import {cidemonNative} from 'lib'; 3 | import {createApiStore} from './Api.store'; 4 | import {IRootStore} from 'Root.store'; 5 | import {when} from 'mobx'; 6 | 7 | jest.mock('../lib/CIDemonNative'); 8 | jest.mock('./Api.store'); 9 | 10 | let ROOT_MOCK: IRootStore = {} as any; 11 | 12 | ROOT_MOCK.api = createApiStore(ROOT_MOCK); 13 | 14 | describe(`NodeStore`, () => { 15 | let nodeStore: Awaited>; 16 | 17 | beforeEach(async () => { 18 | nodeStore = await createNodeStore(ROOT_MOCK as any); 19 | // jest.useFakeTimers(); 20 | }); 21 | 22 | // afterEach(async () => { 23 | // jest.clearAllTimers(); 24 | // }); 25 | 26 | it(`Correctly instantiated and hydrated`, () => { 27 | expect(nodeStore).not.toBeNull(); 28 | // Store hydrates and persists its state 29 | expect(cidemonNative.securelyRetrieve).toBeCalled(); 30 | expect(cidemonNative.securelyStore).toBeCalled(); 31 | // It applies its startAtLogin value 32 | expect(cidemonNative.applyAutoLauncher).toBeCalled(); 33 | }); 34 | 35 | it(`can add token (and trigger fetch)`, () => { 36 | nodeStore.fetchNodes = jest.fn(); 37 | 38 | let name = `CircleCISampleKey`; 39 | let token = `123456789`; 40 | nodeStore.addToken(`CircleCI`, name, token); 41 | 42 | expect(nodeStore.tokens.length).toBe(1); 43 | expect(nodeStore.tokens[0].source).toEqual(`CircleCI`); 44 | expect(nodeStore.tokens[0].name).toEqual(name); 45 | expect(nodeStore.tokens[0].key).toEqual(token); 46 | 47 | // After token has been added fetch nodes is called 48 | expect(nodeStore.fetchNodes).toHaveBeenCalled(); 49 | }); 50 | 51 | it(`toggles sorting in the correct order`, () => { 52 | expect(nodeStore.sortingKey).toEqual(SortingKey.status); 53 | 54 | nodeStore.toggleSorting(); 55 | 56 | expect(nodeStore.sortingKey).toEqual(SortingKey.name); 57 | 58 | nodeStore.toggleSorting(); 59 | 60 | expect(nodeStore.sortingKey).toEqual(SortingKey.date); 61 | 62 | nodeStore.toggleSorting(); 63 | 64 | expect(nodeStore.sortingKey).toEqual(SortingKey.status); 65 | }); 66 | 67 | it(`can remove token by name`, () => { 68 | let name = `CircleCISampleKey`; 69 | let token = `123456789`; 70 | nodeStore.addToken(`CircleCI`, name, token); 71 | 72 | expect(nodeStore.tokens.length).toBe(1); 73 | expect(nodeStore.tokens[0].source).toEqual(`CircleCI`); 74 | expect(nodeStore.tokens[0].name).toEqual(name); 75 | expect(nodeStore.tokens[0].key).toEqual(token); 76 | 77 | nodeStore.removeTokenByName(name); 78 | 79 | expect(nodeStore.tokens.length).toBe(0); 80 | }); 81 | 82 | it(`can add a ignore regex`, () => { 83 | // should pretty much ignore everything under the sun 84 | let testRegex = `.*[A-Za-z]`; 85 | let regexAdded = nodeStore.addIgnoredRegex(testRegex, false); 86 | expect(regexAdded).toBeTruthy(); 87 | expect(nodeStore.complexRegexes.length).toEqual(1); 88 | expect(nodeStore.complexRegexes[0]).toEqual({ 89 | id: nodeStore.complexRegexes[0].id, 90 | regex: testRegex, 91 | inverted: false, 92 | }); 93 | }); 94 | 95 | // TODO: add a test to actually see if nodes are being ignored 96 | // based on the added regex 97 | 98 | it(`can remove regex pattern`, () => { 99 | let testRegex = `.*[A-Za-z]`; 100 | let regexAdded = nodeStore.addIgnoredRegex(testRegex, false); 101 | 102 | expect(regexAdded).toBeTruthy(); 103 | expect(nodeStore.complexRegexes.length).toEqual(1); 104 | expect(nodeStore.complexRegexes[0]).toEqual({ 105 | id: nodeStore.complexRegexes[0].id, 106 | regex: testRegex, 107 | inverted: false, 108 | }); 109 | 110 | nodeStore.removeIgnoredRegex(testRegex); 111 | 112 | expect(nodeStore.complexRegexes.length).toEqual(0); 113 | }); 114 | 115 | it(`does not add a INVALID regex`, () => { 116 | let invalidRegex = `*`; 117 | let regexAdded = nodeStore.addIgnoredRegex(invalidRegex, false); 118 | expect(regexAdded).toBeFalsy(); 119 | }); 120 | 121 | it(`fetch CircleCI nodes`, async (done) => { 122 | let name = `CircleCISampleKey`; 123 | let token = `123456789`; 124 | 125 | let fakeNode: INode = { 126 | id: 'some random node id', 127 | url: 'random url', 128 | date: 'latest date', 129 | source: `CircleCI`, 130 | label: `fake label`, 131 | status: `passed`, 132 | }; 133 | 134 | (ROOT_MOCK.api.fetchCircleciNodes as jest.Mock).mockImplementationOnce(() => 135 | Promise.resolve([fakeNode]), 136 | ); 137 | 138 | nodeStore.addToken(`CircleCI`, name, token); 139 | 140 | expect(ROOT_MOCK.api.fetchCircleciNodes).toHaveBeenCalledWith(token, { 141 | showBuildNumber: false, 142 | }); 143 | 144 | when( 145 | () => nodeStore.fetching === false, 146 | () => { 147 | expect(nodeStore.nodes.length).toBe(1); 148 | expect(nodeStore.nodes[0]).toEqual(fakeNode); 149 | done(); 150 | }, 151 | ); 152 | }); 153 | 154 | it(`fetch AppCenter nodes`, async (done) => { 155 | let name = `AppStore Token`; 156 | let token = `123456789`; 157 | 158 | let fakeNode: INode = { 159 | id: 'some random node id', 160 | url: 'random url', 161 | date: 'latest date', 162 | source: `AppCenter`, 163 | label: `fake label`, 164 | status: `passed`, 165 | }; 166 | 167 | (ROOT_MOCK.api 168 | .fetchAppcenterNodes as jest.Mock).mockImplementationOnce(() => 169 | Promise.resolve([fakeNode]), 170 | ); 171 | 172 | nodeStore.addToken(`AppCenter`, name, token); 173 | 174 | expect(ROOT_MOCK.api.fetchAppcenterNodes).toHaveBeenCalledWith(token, { 175 | showBuildNumber: false, 176 | }); 177 | 178 | when( 179 | () => nodeStore.fetching === false, 180 | () => { 181 | expect(nodeStore.nodes.length).toBe(1); 182 | expect(nodeStore.nodes[0]).toEqual(fakeNode); 183 | done(); 184 | }, 185 | ); 186 | }); 187 | 188 | it(`fetch TravisCI nodes`, async (done) => { 189 | let name = `Travis key`; 190 | let token = `123456789`; 191 | 192 | let fakeNode: INode = { 193 | id: 'some random node id', 194 | url: 'random url', 195 | date: 'latest date', 196 | source: `CircleCI`, 197 | label: `fake label`, 198 | status: `passed`, 199 | }; 200 | 201 | (ROOT_MOCK.api.fetchTravisciNodes as jest.Mock).mockImplementationOnce( 202 | () => { 203 | return Promise.resolve([fakeNode]); 204 | }, 205 | ); 206 | 207 | nodeStore.addToken(`TravisCI`, name, token); 208 | 209 | expect(ROOT_MOCK.api.fetchTravisciNodes).toHaveBeenCalledWith(token, { 210 | showBuildNumber: false, 211 | }); 212 | 213 | when( 214 | () => nodeStore.fetching === false, 215 | () => { 216 | expect(nodeStore.nodes.length).toBe(1); 217 | expect(nodeStore.nodes[0]).toEqual(fakeNode); 218 | done(); 219 | }, 220 | ); 221 | }); 222 | 223 | it(`fetch Bitrise nodes`, async (done) => { 224 | let name = `bitrise key`; 225 | let token = `123456789`; 226 | 227 | let fakeNode: INode = { 228 | id: 'some random node id', 229 | url: 'random url', 230 | date: 'latest date', 231 | source: `CircleCI`, 232 | label: `fake label`, 233 | status: `passed`, 234 | }; 235 | 236 | (ROOT_MOCK.api.fetchBitriseNodes as jest.Mock).mockImplementationOnce( 237 | () => { 238 | return Promise.resolve([fakeNode]); 239 | }, 240 | ); 241 | 242 | nodeStore.addToken(`Bitrise`, name, token); 243 | 244 | expect(ROOT_MOCK.api.fetchBitriseNodes).toHaveBeenCalledWith(token, { 245 | showBuildNumber: false, 246 | }); 247 | 248 | when( 249 | () => nodeStore.fetching === false, 250 | () => { 251 | expect(nodeStore.nodes.length).toBe(1); 252 | expect(nodeStore.nodes[0]).toEqual(fakeNode); 253 | done(); 254 | }, 255 | ); 256 | }); 257 | 258 | it(`fetch Gitlab nodes`, async (done) => { 259 | let name = `Gitlab key`; 260 | let token = `123456789`; 261 | let baseURL = `https://gitlab.com`; 262 | let visibility: GitlabVisibility = `private`; 263 | 264 | let fakeNode: INode = { 265 | id: 'some random node id', 266 | url: 'random url', 267 | date: 'latest date', 268 | source: `Gitlab`, 269 | label: `fake label`, 270 | status: `passed`, 271 | }; 272 | 273 | (ROOT_MOCK.api.fetchGitlabNodes as jest.Mock).mockImplementationOnce(() => { 274 | return Promise.resolve([fakeNode]); 275 | }); 276 | 277 | nodeStore.addToken(`Gitlab`, name, token, baseURL, visibility); 278 | 279 | expect(ROOT_MOCK.api.fetchGitlabNodes).toHaveBeenCalledWith(baseURL, visibility, token, { 280 | showBuildNumber: false, 281 | }); 282 | 283 | when( 284 | () => nodeStore.fetching === false, 285 | () => { 286 | expect(nodeStore.nodes.length).toBe(1); 287 | expect(nodeStore.nodes[0]).toEqual(fakeNode); 288 | done(); 289 | }, 290 | ); 291 | }); 292 | }); 293 | -------------------------------------------------------------------------------- /src/store/Ping.store.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ospfranco/cidemon/8bdd6f9449f98c9e045796c1cb7d195e8c882f78/src/store/Ping.store.ts -------------------------------------------------------------------------------- /src/store/UI.store.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable, runInAction } from 'mobx'; 2 | import { IRootStore } from 'Root.store'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | export let createUIStore = (root: IRootStore) => { 6 | let store = makeAutoObservable({ 7 | toasts: [] as IToast[], 8 | addToast: (toast: IToast) => { 9 | store.toasts.push(toast); 10 | 11 | setTimeout(() => { 12 | runInAction(() => { 13 | store.toasts.pop(); 14 | }); 15 | }, 6000); 16 | }, 17 | clearToasts: () => { 18 | store.toasts = [] 19 | } 20 | // checkForUpdates: () => { 21 | // cidemonNative.checkForUpdates(); 22 | // }, 23 | }); 24 | 25 | return store; 26 | }; 27 | -------------------------------------------------------------------------------- /src/store/__mocks__/Api.store.ts: -------------------------------------------------------------------------------- 1 | import { IRootStore } from "Root.store"; 2 | 3 | export let createApiStore = (_: IRootStore) => { 4 | let store = { 5 | fetchCircleciNodes: jest.fn(), 6 | 7 | fetchAppcenterNodes: jest.fn(), 8 | 9 | fetchTravisciNodes: jest.fn(), 10 | 11 | fetchBitriseNodes: jest.fn(), 12 | 13 | fetchBitriseRepoBranches: jest.fn(), 14 | 15 | fetchGithubNodes: jest.fn(), 16 | 17 | triggerCircleciRebuild: jest.fn(), 18 | 19 | triggerAppcenterRebuild: jest.fn(), 20 | 21 | triggerTravisciRebuild: jest.fn(), 22 | 23 | triggerBitriseRebuild: jest.fn(), 24 | 25 | fetchGitlabNodes: jest.fn(), 26 | }; 27 | 28 | return store; 29 | }; 30 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Api.store" 2 | export * from "./Node.store" 3 | export * from "./UI.store" 4 | -------------------------------------------------------------------------------- /src/tailwind.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'tailwind-rn'; 2 | import styles from 'styles.json'; 3 | import classNames from 'classnames'; 4 | 5 | const {tailwind, getColor} = create(styles); 6 | 7 | export const cw = getColor; 8 | export const tw = (...styles: any[]) => tailwind(classNames(...styles)); 9 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/no-unlimited-disable */ 2 | /* eslint-disable */ 3 | 4 | declare module 'github-api'; 5 | declare module 'base-64'; 6 | declare module 'promise.allsettled' { 7 | function allSettled( 8 | promises: Promise[], 9 | ): Array<{status: 'fulfilled'; value: T} | {status: 'rejected'; reason: any}>; 10 | export = allSettled; 11 | } 12 | 13 | type Awaited> = T extends Promise ? U : never; 14 | 15 | interface IMetrics { 16 | pxs: number; 17 | ps: number; 18 | pm: number; 19 | pxm: number; 20 | pl: number; 21 | ts: number; 22 | tm: number; 23 | tl: number; 24 | imgSmall: number; 25 | imgMedium: number; 26 | imgLarge: number; 27 | } 28 | 29 | declare var global: { 30 | metrics: IMetrics; 31 | colors: any; 32 | os: 'ios' | 'macos' | 'android' | 'web' | 'windows'; 33 | }; 34 | 35 | declare module '*.png'; 36 | declare module '*.jpg'; 37 | declare module '*.jpeg'; 38 | declare interface IIgnoreRegex { 39 | id: string; 40 | regex: string; 41 | inverted: boolean; 42 | } 43 | 44 | declare type Source = 45 | | `CircleCI` 46 | | `AppCenter` 47 | | `TravisCI` 48 | | `Bitrise` 49 | | `Github` 50 | | `Gitlab`; 51 | 52 | declare type Status = `pending` | `passed` | `running` | `failed`; 53 | 54 | declare type VCS = `github` | `bitbucket` | `gitlab` | `unknown`; 55 | 56 | declare type GitlabVisibility = `public` | `internal` | `private` | null; 57 | 58 | declare interface IToken { 59 | source: Source; 60 | name: string; 61 | baseURL?: string | null; 62 | visibility?: GitlabVisibility; 63 | key: string; 64 | } 65 | 66 | declare interface ISubNode { 67 | label: string; 68 | extraLabel?: string | null; 69 | status: Status; 70 | url?: string; 71 | } 72 | 73 | declare interface INode { 74 | id: string; 75 | source: Source; 76 | url: string; 77 | label: string; 78 | status: Status; 79 | vcs?: VCS; 80 | key?: string; 81 | buildUrl?: string; 82 | date?: string; 83 | jobId?: string; 84 | // used for some extra field needed to make requests to the api 85 | extra?: string; 86 | subItems?: ISubNode[]; 87 | // added fields for small nice things 88 | sha?: string; 89 | isPr?: boolean; 90 | isAction?: boolean; 91 | isBranch?: boolean; 92 | username?: string; 93 | userAvatarUrl?: string; 94 | slug?: string; 95 | } 96 | 97 | declare interface IToast { 98 | text: string; 99 | type: `success` | `error` | `neutral`; 100 | position?: `top` | `bottom`; 101 | } 102 | 103 | declare interface IPingTestDto { 104 | id?: string; 105 | name?: string; 106 | url?: string; 107 | method?: `GET` | `POST` | `PUT`; 108 | expectedStatus?: number | null; 109 | expectedResponse?: string | null; 110 | } 111 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | purge: [], 5 | darkMode: false, // or 'media' or 'class' 6 | theme: { 7 | extend: { 8 | colors: {...colors}, 9 | pass: { 10 | 50: '#F0F9F8', 11 | 100: '#E8F9FA', 12 | 200: '#99F6E4', 13 | 300: '#5EEAD4', 14 | 400: '#2DD4BF', 15 | 500: '#16BDCA', 16 | 600: '#0891B2', 17 | 700: '#0F766E', 18 | 800: '#1E383C', 19 | 900: '#1E3034', 20 | }, 21 | }, 22 | }, 23 | variants: { 24 | extend: {}, 25 | }, 26 | plugins: [], 27 | }; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ 8 | "ES2020" 9 | ], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": false, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | "removeComments": true, /* Do not emit comments to output. */ 19 | "noEmit": true, /* Do not emit outputs. */ 20 | // "incremental": true, /* Enable incremental compilation */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | 40 | /* Module Resolution Options */ 41 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": { }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": ["./src/typings", "node_modules/@types"], /* List of folders to include type definitions from. */ 46 | // "types": ["jest"], /* Type declaration files to be included in compilation. */ 47 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 50 | 51 | /* Source Map Options */ 52 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | "useDefineForClassFields": true, /* required for mobx 6*/ 61 | "resolveJsonModule": true, 62 | "skipLibCheck": true 63 | }, 64 | "exclude": [ 65 | "node_modules", "babel.config.js", "metro.config.js", "jest.config.js" 66 | ] 67 | } 68 | --------------------------------------------------------------------------------