diff --git a/package-lock.json b/package-lock.json index 3be1785..69e5c99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "admob-sync-app", - "version": "0.1.5", + "version": "0.1.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4383,6 +4383,12 @@ } } }, + "electron-tooltip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/electron-tooltip/-/electron-tooltip-1.1.5.tgz", + "integrity": "sha512-H3vxGOkwxVrJdhJHiJ0i01EOny0FSz2+e68rNK21LA4quZcThFIUUXSSIEMCKLwe3Iyd87+AWdpTg1b9teruQg==", + "dev": true + }, "elliptic": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", @@ -5538,8 +5544,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5560,14 +5565,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5582,20 +5585,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5712,8 +5712,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5725,7 +5724,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5740,7 +5738,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5748,14 +5745,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5774,7 +5769,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5855,8 +5849,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5868,7 +5861,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5954,8 +5946,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5991,7 +5982,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6011,7 +6001,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6055,14 +6044,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6818,8 +6805,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6840,14 +6826,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6862,20 +6846,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6992,8 +6973,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -7005,7 +6985,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7020,7 +6999,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7028,14 +7006,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7054,7 +7030,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7135,8 +7110,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -7148,7 +7122,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -7234,8 +7207,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -7271,7 +7243,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7291,7 +7262,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7335,14 +7305,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -11144,6 +11112,28 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==" }, + "react-minimalist-portal": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-minimalist-portal/-/react-minimalist-portal-2.3.1.tgz", + "integrity": "sha1-SFPj9Ip0oywbh2dgGIfN95Qe66M=", + "requires": { + "prop-types": "^15.6.1" + } + }, + "react-tiny-accordion": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-tiny-accordion/-/react-tiny-accordion-2.0.4.tgz", + "integrity": "sha512-yp9bnxVuwssh1TbnFl0IpZoq2TnmTtuVazfH3FJpk60Ex4VitRzUE+NU0fuMnnil4g3Y0gjyhE/0H1/xqdzWWQ==" + }, + "react-tooltip-lite": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.9.1.tgz", + "integrity": "sha512-JH5T6kPZn7X90TnnNhuJ+wOb1eikT2xtpbOkndvqAHZlOyZOAZeAyVgk/3pGz0xi4h+bqXXisfwGtriliTYhDQ==", + "requires": { + "prop-types": "^15.5.8", + "react-minimalist-portal": "^2.2.0" + } + }, "read-chunk": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-1.0.1.tgz", diff --git a/package.json b/package.json index 68273af..ac880c5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "repository": "https://github.com/appodeal/admob-sync-app", "description": "Appodeal AdMob Sync application", "private": true, - "version": "0.1.5", + "version": "0.1.6", "scripts": { "start": "webpack --watch --progress --config=webpack/development.ts", "test": "jest", @@ -129,9 +129,11 @@ "electron-builder": "^20.38.5", "file-loader": "^3.0.1", "generate-json-webpack-plugin": "^0.3.1", + "github": "~0.2.4", "html-webpack-plugin": "^3.2.0", "jest": "^24.1.0", "mini-css-extract-plugin": "^0.5.0", + "moment": "^2.22.2", "node-sass": "^4.11.0", "sass-loader": "^7.1.0", "script-ext-html-webpack-plugin": "^2.1.3", @@ -142,9 +144,7 @@ "webpack": "^4.29.5", "webpack-cli": "^3.2.3", "webpack-dev-server": "^3.2.0", - "webpack-merge": "^4.2.1", - "github": "~0.2.4", - "moment": "^2.22.2" + "webpack-merge": "^4.2.1" }, "dependencies": { "@sentry/electron": "^0.16.0", @@ -165,6 +165,8 @@ "raven-js": "^3.27.0", "react": "^16.8.3", "react-dom": "^16.8.3", + "react-tiny-accordion": "^2.0.4", + "react-tooltip-lite": "^1.9.1", "semver": "^6.0.0", "uuid": "^3.3.2", "winston": "^3.2.1", diff --git a/src/core/appdeal-api/appodeal-api.factory.ts b/src/core/appdeal-api/appodeal-api.factory.ts index 1c2db37..453c472 100644 --- a/src/core/appdeal-api/appodeal-api.factory.ts +++ b/src/core/appdeal-api/appodeal-api.factory.ts @@ -47,8 +47,11 @@ export class AppodealApi { throw err; }); if (account) { + this.destroyApi(account.id); this.saveApi(api, account); sessionInfo.save(account.id); + } else { + api.destroy(); } return account; } @@ -63,10 +66,7 @@ export class AppodealApi { error.isHandled = true; } }); - api.destroy(); - this.APIs.delete(accountId); - this.errorSubscriptions.get(accountId).unsubscribe(); - this.errorSubscriptions.delete(accountId); + this.destroyApi(accountId); await AppodealSessions.remove(accountId); } @@ -81,6 +81,17 @@ export class AppodealApi { )); } + private destroyApi (accountId: string) { + if (this.APIs.has(accountId)) { + this.APIs.get(accountId).destroy(); + this.APIs.delete(accountId); + } + if (this.errorSubscriptions.has(accountId)) { + this.errorSubscriptions.get(accountId).unsubscribe(); + this.errorSubscriptions.delete(accountId); + } + } + private saveApi (api: AppodealApiService, account: UserAccount) { api.init(account.id); this.APIs.set(account.id, api); diff --git a/src/core/appdeal-api/appodeal-api.service.ts b/src/core/appdeal-api/appodeal-api.service.ts index f708c09..7f6db0a 100644 --- a/src/core/appdeal-api/appodeal-api.service.ts +++ b/src/core/appdeal-api/appodeal-api.service.ts @@ -6,7 +6,7 @@ import {ErrorResponse, onError} from 'apollo-link-error'; import {AuthContext, TokensInfo} from 'core/appdeal-api/auth-context'; import {AppodealAccount} from 'core/appdeal-api/interfaces/appodeal.account.interface'; import {AuthorizationError} from 'core/error-factory/errors/authorization.error'; -import {session, Session} from 'electron'; +import {Session} from 'electron'; import {ExtractedAdmobAccount} from 'interfaces/common.interfaces'; import {createFetcher, Fetcher} from 'lib/fetch'; import {AdMobApp} from 'lib/translators/interfaces/admob-app.interface'; @@ -15,9 +15,9 @@ import {ErrorFactoryService} from '../error-factory/error-factory.service'; import {InternalError} from '../error-factory/errors/internal-error'; import addAdMobAccountMutation from './graphql/add-admob-account.mutation.graphql'; import adMobAccountQuery from './graphql/admob-account-details.graphql'; -import minimalAppVersionQuery from './graphql/minimal-app-version.query.graphql'; import currentUserQuery from './graphql/current-user.query.graphql'; import endSync from './graphql/end-sync.mutation.graphql'; +import minimalAppVersionQuery from './graphql/minimal-app-version.query.graphql'; import pingQuery from './graphql/ping.query.graphql'; import refreshTokenMutation from './graphql/refresh-token-mutation.graphql'; @@ -133,7 +133,7 @@ export class AppodealApiService { } destroy () { - this.authContext.removeAllListeners(); + this.authContext.destroy(); } handleError (e: InternalError) { @@ -197,7 +197,7 @@ export class AppodealApiService { fetchCurrentUser (): Promise { return this.query<{ currentUser: AppodealAccount }>({ - query: currentUserQuery, + query: currentUserQuery }) .then(result => { return result.currentUser; diff --git a/src/core/appdeal-api/auth-context.ts b/src/core/appdeal-api/auth-context.ts index 40ab184..d666508 100644 --- a/src/core/appdeal-api/auth-context.ts +++ b/src/core/appdeal-api/auth-context.ts @@ -39,6 +39,7 @@ export class AuthContext extends EventEmitter { private accessToken: string = null; private refreshToken: string = null; private accountId: string = null; + private refreshInterval = null; init (accountId: string) { this.setAccountId(accountId); @@ -50,7 +51,7 @@ export class AuthContext extends EventEmitter { console.log(`NO auth tokens for ${accountId}`); } // check should we update refresh token each 1 minute - setInterval(() => this.emitRefresh(), 60000); + this.refreshInterval = setInterval(() => this.emitRefresh(), 60000); this.emitRefresh(); } @@ -110,4 +111,9 @@ export class AuthContext extends EventEmitter { }; }); } + + destroy () { + clearInterval(this.refreshInterval); + this.removeAllListeners(); + } } diff --git a/src/core/logs-connector.ts b/src/core/logs-connector.ts index 4d8ee9e..99faab1 100644 --- a/src/core/logs-connector.ts +++ b/src/core/logs-connector.ts @@ -2,12 +2,9 @@ import {AppodealApi} from 'core/appdeal-api/appodeal-api.factory'; import {AdMobAccount} from 'core/appdeal-api/interfaces/admob-account.interface'; import {Connector} from 'core/connector'; import {Store} from 'core/store'; +import {shell} from 'electron'; import {ActionTypes, LogAction} from 'lib/actions'; -import {getLogContent, getLogsDirectory, LogFileInfo} from 'lib/sync-logs/logger'; - - -const shell = require('electron').shell; -const path = require('path'); +import {getLogContent, logFilePathName} from 'lib/sync-logs/logger'; export class LogsConnector extends Connector { @@ -18,25 +15,25 @@ export class LogsConnector extends Connector { async onAction ({type, payload}: LogAction) { switch (type) { case ActionTypes.openLogFile: - this.openLog(payload.account, payload.log); + this.openLog(payload.account, payload.syncId); return payload; case ActionTypes.submitLogToAppodeal: - return this.submitLog(payload.account, payload.log, payload.appodealAccountId); + return this.submitLog(payload.account, payload.syncId, payload.appodealAccountId); } } - openLog (account: AdMobAccount, log: LogFileInfo) { - console.info('openLog', log); + openLog (account: AdMobAccount, syncId: string) { + console.info('openLog', syncId); try { - shell.openItem(path.join(getLogsDirectory(account), log.fileName)); + shell.openItem(logFilePathName(account, syncId)); } catch (e) { console.error(e); } } - async submitLog (account: AdMobAccount, log: LogFileInfo, appodealAccountId: string) { - const rawLog = await getLogContent(account, log.uuid); - return this.appodealApi.getFor(appodealAccountId).submitLog(account.id, log.uuid, rawLog); + async submitLog (account: AdMobAccount, syncId: string, appodealAccountId: string) { + const rawLog = await getLogContent(account, syncId); + return this.appodealApi.getFor(appodealAccountId).submitLog(account.id, syncId, rawLog); } } diff --git a/src/core/store.ts b/src/core/store.ts index d65a285..f5b9d39 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -14,7 +14,6 @@ import {ActionTypes} from 'lib/actions'; import {AppPreferences, Preferences} from 'lib/app-preferences'; import {deepAssign} from 'lib/core'; import {onActionFromRenderer} from 'lib/messages'; -import {getLogsList, LogFileInfo} from 'lib/sync-logs/logger'; import {openSettingsWindow} from 'lib/ui-windows'; import {confirmDialog, messageDialog, openWindow, waitForNavigation} from 'lib/window'; import {action, observable, observe, set} from 'mobx'; @@ -33,7 +32,6 @@ export interface SyncProgress { export interface AppState { selectedAccount: { account: AdMobAccount; - logs: LogFileInfo[]; } selectedAppodealAccount: AppodealAccount; syncHistory: Record; @@ -50,8 +48,7 @@ export class Store { @observable readonly state: AppState = { selectedAccount: { - account: null, - logs: [] + account: null }, selectedAppodealAccount: null, syncHistory: {}, @@ -87,13 +84,13 @@ export class Store { this.onlineService.on('statusChange', isOnline => { set(this.state, 'online', isOnline); }); - this.onlineService.on('nextReconnect' , (time) => { + this.onlineService.on('nextReconnect', (time) => { set(this.state, 'nextReconnect', time); }); } - public updateUserWhenOnline() { - this.onlineService.on('online',() => { + public updateUserWhenOnline () { + this.onlineService.on('online', () => { this.fetchAllAppodealUsers(); }); } @@ -205,14 +202,9 @@ export class Store { } @action - async pushLogs (account: AdMobAccount, logs?: Array) { - logs = Array.isArray(logs) ? logs : await this.loadSelectedAdMobAccountLogs(account); - let selectedAccount = this.state.selectedAccount.account; - if (account && selectedAccount && account.id === selectedAccount.id) { - set(this.state, 'selectedAccount', { - logs, - account - }); + async pushLogs (account: AdMobAccount) { + if (account) { + return this.updateAdMobAccountInfo(account); } } @@ -241,6 +233,7 @@ export class Store { percent: (pEvent.synced + pEvent.failed) / pEvent.total * 100, lastEvent: event.type }; + this.pushLogs(account); return this.fireSyncUpdated(); case SyncEventsTypes.Stopped: delete this.state.syncProgress[event.accountId]; @@ -319,28 +312,14 @@ export class Store { } @action - selectAdMobAccount (newAccount: AdMobAccount) { - let {account} = this.state.selectedAccount; + selectAdMobAccount (newAccount: AdMobAccount | null) { set(this.state, 'selectedAccount', { - account: newAccount, - logs: account && newAccount && account.id === newAccount.id ? this.state.selectedAccount.logs : [] + account: newAccount }); // we dont need to wait while logs are loading // we want provide quick response to UI // once log-list is loaded - we will show it - if (newAccount) { - setTimeout(() => this.pushLogs(newAccount)); - } - } - - loadSelectedAdMobAccountLogs (account: AdMobAccount): Promise { - if (!account) { - return Promise.resolve([]); - } - return getLogsList(account).catch(e => { - console.error(e); - return []; - }); + setTimeout(() => this.pushLogs(newAccount)); } @action diff --git a/src/core/sync-apps/sync-history.ts b/src/core/sync-apps/sync-history.ts index 12de6f8..d84b455 100644 --- a/src/core/sync-apps/sync-history.ts +++ b/src/core/sync-apps/sync-history.ts @@ -1,5 +1,6 @@ import {AdMobAccount} from 'core/appdeal-api/interfaces/admob-account.interface'; import {Sync} from 'core/sync-apps/sync'; +import {SyncInfo} from 'core/sync-apps/sync-stats'; import {ExtractedAdmobAccount} from 'interfaces/common.interfaces'; import {getJsonFile, saveJsonFile} from 'lib/json-storage'; import path from 'path'; @@ -8,7 +9,8 @@ import path from 'path'; export interface SyncHistoryInfo { lastSync: number lastSuccessfulSync: number; - admobAuthorizationRequired: boolean + admobAuthorizationRequired: boolean, + syncs: SyncInfo[]; } @@ -26,10 +28,12 @@ export class SyncHistory { private static async loadHistory (adMobAccount: AdmobAccount): Promise { if (!SyncHistory.cache.has(adMobAccount.id)) { const data = await getJsonFile(SyncHistory.fileName(adMobAccount)); + data.syncs = data.syncs || []; SyncHistory.cache.set(adMobAccount.id, data || { lastSync: null, lastSuccessfulSync: null, - admobAuthorizationRequired: true + admobAuthorizationRequired: true, + syncs: [] }); } @@ -46,13 +50,19 @@ export class SyncHistory { await SyncHistory.saveHistory(adMobAccount); } - public static async logSyncEnd (sync: Sync) { + public static async saveSyncStats (sync: Sync) { const history = await SyncHistory.loadHistory(sync.adMobAccount); const time = Date.now(); history.lastSync = time; if (!sync.hasErrors) { history.lastSuccessfulSync = time; } + const statIndex = history.syncs.findIndex(stats => stats.id === sync.id); + if (statIndex === -1) { + history.syncs.unshift(sync.stats.toPlainObject()); + } else { + history.syncs[statIndex] = sync.stats.toPlainObject(); + } await SyncHistory.saveHistory(sync.adMobAccount); } diff --git a/src/core/sync-apps/sync-scheduler.ts b/src/core/sync-apps/sync-scheduler.ts index 2088ffe..c20a5d9 100644 --- a/src/core/sync-apps/sync-scheduler.ts +++ b/src/core/sync-apps/sync-scheduler.ts @@ -1,7 +1,7 @@ import {OnlineService} from 'core/appdeal-api/online.service'; import {Store} from 'core/store'; import {SyncHistory} from 'core/sync-apps/sync-history'; -import {SyncService} from 'core/sync-apps/sync.service'; +import {SyncRunner, SyncService} from 'core/sync-apps/sync.service'; import {timeConversion} from 'lib/time'; import {observe} from 'mobx'; @@ -52,7 +52,7 @@ export class SyncScheduler { unsubscribe = null; this.store.state.selectedAppodealAccount.accounts.forEach(adMobAccount => { this.log(`App started. Run sync for Admob Account [${adMobAccount.id} ${adMobAccount.email}]`); - this.syncService.runSync(this.store.state.selectedAppodealAccount.id, adMobAccount) + this.syncService.runSync(this.store.state.selectedAppodealAccount.id, adMobAccount, SyncRunner.SyncScheduler) .catch(err => { }); @@ -75,7 +75,7 @@ export class SyncScheduler { if (!lastSync) { this.log(`Admob Account [${adMobAccount.id} ${adMobAccount.email}] has never synced. Run sync.`); - return this.syncService.runSync(this.store.state.selectedAppodealAccount.id, adMobAccount) + return this.syncService.runSync(this.store.state.selectedAppodealAccount.id, adMobAccount, SyncRunner.SyncScheduler) .catch(err => { }); @@ -86,7 +86,7 @@ export class SyncScheduler { timeConversion(scienceLastSync) }. Run sync.` ); - return this.syncService.runSync(this.store.state.selectedAppodealAccount.id, adMobAccount) + return this.syncService.runSync(this.store.state.selectedAppodealAccount.id, adMobAccount, SyncRunner.SyncScheduler) .catch(err => { }); diff --git a/src/core/sync-apps/sync-stats.ts b/src/core/sync-apps/sync-stats.ts new file mode 100644 index 0000000..1d8deff --- /dev/null +++ b/src/core/sync-apps/sync-stats.ts @@ -0,0 +1,87 @@ +import {AppodealApp} from 'core/appdeal-api/interfaces/appodeal-app.interface'; +import {Sync} from 'core/sync-apps/sync'; +import {SyncRunner} from 'core/sync-apps/sync.service'; + + +export type AppInfo = Pick & Pick; + + +export interface SyncInfo { + readonly id: string; + readonly startTs: number + readonly endTs: number; + readonly terminated: boolean; + readonly hasErrors: boolean; + // if only some apps where synced + readonly partialSync: boolean; + affectedApps: { + created: AppInfo[]; + updated: AppInfo[]; + deleted: AppInfo[]; + withErrors: AppInfo[]; + }, + runner: SyncRunner; + logSubmitted?: boolean; +} + +export class SyncStats { + + public readonly affectedApps = { + created: [], + updated: [], + deleted: [], + withErrors: [] + }; + + public id: string; + + constructor (private sync: Sync) {} + + public startTs: number; + public endTs: number; + + public terminated = false; + public partialSync = false; + + start () { + this.startTs = Date.now(); + } + + end () { + this.endTs = Date.now(); + } + + appCreated (app: AppodealApp) { + this.affectedApps.created.push(app); + } + + appUpdated (app: AppodealApp) { + if (this.affectedApps.created.some(v => v.id === app.id)) { + return; + } + this.affectedApps.updated.push(app); + } + + appDeleted (app: AppodealApp) { + this.affectedApps.deleted.push(app); + } + + errorWhileSync (app: AppodealApp) { + this.affectedApps.withErrors.push(app); + } + + toPlainObject (): SyncInfo { + return { + id: this.sync.id, + runner: this.sync.runner, + hasErrors: this.sync.hasErrors, + affectedApps: this.affectedApps, + startTs: this.startTs, + endTs: this.endTs, + terminated: this.terminated, + partialSync: this.partialSync + + }; + } + +} diff --git a/src/core/sync-apps/sync.service.ts b/src/core/sync-apps/sync.service.ts index ca95181..ac840b8 100644 --- a/src/core/sync-apps/sync.service.ts +++ b/src/core/sync-apps/sync.service.ts @@ -14,6 +14,11 @@ import {createSyncLogger, getLogContent, LoggerInstance, rotateSyncLogs} from 'l import uuid from 'uuid'; +export enum SyncRunner { + User = 1, + SyncScheduler = 2 +} + type FinishPromise = Promise; export class SyncService { @@ -33,7 +38,7 @@ export class SyncService { } - public async runSync (appodealAccountId: string, admobAccount: AdMobAccount) { + public async runSync (appodealAccountId: string, admobAccount: AdMobAccount, runner: SyncRunner) { return new Promise(async (resolve, reject) => { if (this.onlineService.isOffline()) { console.log('[Sync Service] Can not run sync. No Internet Connection'); @@ -70,7 +75,8 @@ export class SyncService { admobAccount, appodealAccountId, logger, - id + id, + runner ); const waitToFinish = []; @@ -143,7 +149,7 @@ export class SyncService { console.error(e); } try { - await SyncHistory.logSyncEnd(sync); + await SyncHistory.saveSyncStats(sync); } catch (e) { console.error('Failed to save sync history'); this.reportError(sync, e); diff --git a/src/core/sync-apps/sync.ts b/src/core/sync-apps/sync.ts index a968fc2..976c269 100644 --- a/src/core/sync-apps/sync.ts +++ b/src/core/sync-apps/sync.ts @@ -4,6 +4,9 @@ import {AppodealApiService} from 'core/appdeal-api/appodeal-api.service'; import {AdMobAccount} from 'core/appdeal-api/interfaces/admob-account.interface'; import {AdType, AppodealAdUnit, AppodealApp, AppodealPlatform, Format} from 'core/appdeal-api/interfaces/appodeal-app.interface'; import {getAdUnitTemplate} from 'core/sync-apps/ad-unit-templates'; +import {SyncHistory} from 'core/sync-apps/sync-history'; +import {SyncStats} from 'core/sync-apps/sync-stats'; +import {SyncRunner} from 'core/sync-apps/sync.service'; import stringify from 'json-stable-stringify'; import {retry} from 'lib/retry'; import {AppTranslator} from 'lib/translators/admob-app.translator'; @@ -44,6 +47,9 @@ export class Sync { public hasErrors = false; private terminated = true; + + public readonly stats = new SyncStats(this); + public context = new SyncContext(); /** @@ -58,7 +64,8 @@ export class Sync { private appodealAccountId: string, private logger: Partial, // some uniq syncId - public readonly id: string + public readonly id: string, + public readonly runner: SyncRunner ) { this.id = id || uuid.v4(); } @@ -69,13 +76,16 @@ export class Sync { return; } this.terminated = true; + this.stats.terminated = true; this.logger.info(`Stopping Sync Reason: ${reason}`); } async run () { this.logger.info(`Sync started`); this.terminated = false; + this.stats.start(); try { + await SyncHistory.saveSyncStats(this); for await (const value of this.doSync()) { this.logger.info(value); if (this.terminated) { @@ -93,6 +103,7 @@ export class Sync { } finish () { + this.stats.end(); this.emit(SyncEventsTypes.Stopped); this.appodealApi.reportSyncEnd(this.id); } @@ -123,8 +134,11 @@ export class Sync { this.emit(SyncEventsTypes.Started); this.logger.info(`Sync Params uuid: ${this.id} - AppodealAccount: '${this.appodealAccountId}}' - AdmobAccount: '${this.adMobAccount.id}}': '${this.adMobAccount.email}}' + AppodealAccount: + id: ${this.appodealAccountId} + AdmobAccount: + id: ${this.adMobAccount.id} + email: ${this.adMobAccount.email} `); await this.appodealApi.reportSyncStart(this.id, this.adMobAccount.id); try { @@ -161,7 +175,7 @@ export class Sync { this.emitError(e); } this.emit(SyncEventsTypes.UserActionsRequired); - this.terminated = true; + await this.stop('Terminated as User Actions is Required'); yield 'Terminated as User Actions is Required'; return; } @@ -209,6 +223,7 @@ export class Sync { yield* this.syncApp(app); synced++; } catch (e) { + this.stats.errorWhileSync(app); failed++; this.logger.error(e); this.emitError(e); @@ -241,6 +256,7 @@ export class Sync { const adUnitsToDelete = this.getActiveAdmobAdUnitsCreatedByApp(app, adMobApp).map(adUnit => adUnit.adUnitId); if (adUnitsToDelete.length) { await this.deleteAdMobAdUnits(adUnitsToDelete); + this.stats.appDeleted(app); yield `${adUnitsToDelete.length} adUnits deleted`; } else { yield `No AdUnits to delete`; @@ -252,6 +268,7 @@ export class Sync { yield `Hide App. All its adUnits are archived`; adMobApp = await this.hideAdMobApp(adMobApp); this.context.updateAdMobApp(adMobApp); + this.stats.appDeleted(app); yield `App Hidden`; } @@ -275,12 +292,14 @@ export class Sync { this.logger.info(`Unable to find App. Try to create new`); adMobApp = await this.createAdMobApp(app); this.context.addAdMobApp(adMobApp); + this.stats.appCreated(app); yield `App created`; } if (adMobApp.hidden) { adMobApp = await this.showAdMobApp(adMobApp); this.context.updateAdMobApp(adMobApp); + this.stats.appUpdated(app); yield `Hidden App has been shown`; } @@ -351,6 +370,7 @@ export class Sync { }); if (newAdUnit) { this.context.addAdMobAdUnit(newAdUnit); + this.stats.appUpdated(app); appodealAdUnits.push(this.convertToAppodealAdUnit(newAdUnit, adUnitTemplate)); yield `AdUnit Created ${this.adUnitCode(newAdUnit)} ${adUnitTemplate.name}`; } @@ -581,7 +601,7 @@ export class Sync { const publishedApp = searchAppResponse.find(publishedApp => publishedApp.applicationStoreId === app.bundleId); if (publishedApp) { this.logger.info(`App found in store`); - + this.stats.appUpdated(app); adMobApp = {...adMobApp, ...publishedApp}; return await this.adMobApi.postRaw('AppService', 'Update', { 1: getTranslator(AppTranslator).encode(adMobApp), diff --git a/src/core/sync-connector.ts b/src/core/sync-connector.ts index d20a018..186f205 100644 --- a/src/core/sync-connector.ts +++ b/src/core/sync-connector.ts @@ -1,6 +1,6 @@ import {Connector} from 'core/connector'; import {Store} from 'core/store'; -import {SyncService} from 'core/sync-apps/sync.service'; +import {SyncRunner, SyncService} from 'core/sync-apps/sync.service'; import {Action, ActionTypes} from 'lib/actions'; @@ -15,7 +15,7 @@ export class SyncConnector extends Connector { switch (type) { case ActionTypes.runSync: if (this.syncService.canRun(payload.adMobAccount)) { - return this.syncService.runSync(payload.appodealAccountId, payload.adMobAccount); + return this.syncService.runSync(payload.appodealAccountId, payload.adMobAccount, SyncRunner.User); } return; default: diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 3317f15..72fdacf 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,5 +1,4 @@ import {AdMobAccount} from 'core/appdeal-api/interfaces/admob-account.interface'; -import {LogFileInfo} from 'lib/sync-logs/logger'; export enum ActionTypes { @@ -37,7 +36,7 @@ export interface LogAction extends Action { type: ActionTypes.openLogFile | ActionTypes.submitLogToAppodeal, payload: { account: AdMobAccount, - log: LogFileInfo, + syncId: string, appodealAccountId: string } } diff --git a/src/lib/app-menu.ts b/src/lib/app-menu.ts index 06e1195..4ca9e4a 100644 --- a/src/lib/app-menu.ts +++ b/src/lib/app-menu.ts @@ -1,4 +1,5 @@ import {Menu} from 'electron'; +import {showAboutDialog} from 'lib/about'; import {isMacOS} from 'lib/platform'; @@ -17,7 +18,7 @@ class AppMenu { { label: 'Application', submenu: [ - {label: 'About Application', role: 'about:'}, + {label: 'About Application', click: () => showAboutDialog()}, {type: 'separator'}, {label: 'Quit', accelerator: 'Command+Q', role: 'quit'} ] diff --git a/src/lib/sync-logs/logger.ts b/src/lib/sync-logs/logger.ts index eb00b92..6ef8a3d 100644 --- a/src/lib/sync-logs/logger.ts +++ b/src/lib/sync-logs/logger.ts @@ -49,7 +49,7 @@ export function getLogsDirectory (adMobAccount: AdMobAccount) { } -function logFilePathName (adMobAccount: AdMobAccount, syncId: string) { +export function logFilePathName (adMobAccount: AdMobAccount, syncId: string) { return path.join(getLogsDirectory(adMobAccount), `${syncId}.log`); } @@ -123,7 +123,7 @@ export async function getLogsList (adMobAccount: AdMobAccount): Promise fs.unlink(path.join(dir, f.fileName))) ); - } diff --git a/src/lib/ui-windows.ts b/src/lib/ui-windows.ts index 7c89c2f..8972be0 100644 --- a/src/lib/ui-windows.ts +++ b/src/lib/ui-windows.ts @@ -1,5 +1,5 @@ import {AppodealAccount} from 'core/appdeal-api/interfaces/appodeal.account.interface'; -import {BrowserWindow} from 'electron'; +import {app, BrowserWindow} from 'electron'; import {AppodealAccountState} from 'interfaces/common.interfaces'; import {getMapItem} from 'lib/core'; import {openDialogWindow, openWindow} from 'lib/window'; @@ -10,8 +10,13 @@ const OPENED_WINDOWS = new Map>() export async function openSettingsWindow () { return openOrFocus('settings', async () => { let window = await openWindow('./settings.html', { - fullscreenable: false - }, () => window.removeAllListeners()); + fullscreenable: false, + skipTaskbar: false + }, () => { + app.dock.hide(); + window.removeAllListeners(); + }); + app.dock.show(); window.on('focus', async event => { let topWindow = getMapItem(OPENED_WINDOWS, OPENED_WINDOWS.size - 1); if (topWindow) { @@ -36,6 +41,13 @@ export function openAppodealSignInWindow (account: AppodealAccountState = null): './sign-in.html', {width: 450, height: 270, parent: await OPENED_WINDOWS.get('settings')}, window => { + let parent = window.getParentWindow(); + app.dock.show(); + window.once('closed', () => { + if (!parent) { + app.dock.hide(); + } + }); window.webContents.send('existingAccount', JSON.stringify(account)); res(window); } @@ -50,7 +62,16 @@ export function openAppodealAccountsWindow (): Promise { openDialogWindow( './manage-accounts.html', {width: 450, height: 350, parent: await OPENED_WINDOWS.get('settings')}, - res + window => { + let parent = window.getParentWindow(); + app.dock.show(); + window.once('closed', () => { + if (!parent) { + app.dock.hide(); + } + }); + res(window); + } ).then(resolve, reject); })); }); diff --git a/src/lib/window.ts b/src/lib/window.ts index 1e921a3..4f1753a 100644 --- a/src/lib/window.ts +++ b/src/lib/window.ts @@ -4,9 +4,9 @@ import {getBgColor} from './theme'; function getConfig (config: BrowserWindowConstructorOptions, backgroundColor: string): BrowserWindowConstructorOptions { return { - width: 750, + width: 800, height: 550, - minWidth: 750, + minWidth: 800, minHeight: 550, frame: false, titleBarStyle: 'hiddenInset', @@ -69,8 +69,6 @@ export function openDialogWindow ( height, minWidth: width, minHeight: height, - // maxWidth: width, - // maxHeight: height, resizable: false, frame: true, fullscreenable: false, @@ -96,35 +94,6 @@ export function openDialogWindow ( }); } -export function createScript (fn: (...args: Array) => void, ...args) { - return `(async function (...args) { - let safeJsonParse = json => { - let result; - try { - result = JSON.parse(json); - } catch (e) { - result = json; - } - return result; - }; - return (${fn.toString()})(...args.map(arg => { - if (typeof arg === 'function') { - return arg; - } else { - return safeJsonParse(arg); - } - })); - })(${args.map(arg => { - if (typeof arg === 'function') { - return arg.toString(); - } else if (typeof arg === 'string') { - return `'${arg}'`; - } else { - return `'${JSON.stringify(arg)}'`; - } - }).join(', ')})`; -} - export function waitForNavigation (window: BrowserWindow, urlFragment: RegExp = null): Promise { return new Promise(resolve => { let resolver = () => { diff --git a/src/ui/assets/images/eye.svg b/src/ui/assets/images/eye.svg new file mode 100644 index 0000000..c7e5988 --- /dev/null +++ b/src/ui/assets/images/eye.svg @@ -0,0 +1,3 @@ + + diff --git a/src/ui/assets/images/sync-status/check-round.svg b/src/ui/assets/images/sync-status/check-round.svg new file mode 100644 index 0000000..a9f6dbf --- /dev/null +++ b/src/ui/assets/images/sync-status/check-round.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/src/ui/assets/images/sync-status/clock.svg b/src/ui/assets/images/sync-status/clock.svg new file mode 100644 index 0000000..d0b6439 --- /dev/null +++ b/src/ui/assets/images/sync-status/clock.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/ui/assets/images/sync-status/times-round.svg b/src/ui/assets/images/sync-status/times-round.svg new file mode 100644 index 0000000..33cb499 --- /dev/null +++ b/src/ui/assets/images/sync-status/times-round.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/ui/assets/images/sync-status/user.svg b/src/ui/assets/images/sync-status/user.svg new file mode 100644 index 0000000..a0813ab --- /dev/null +++ b/src/ui/assets/images/sync-status/user.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/src/ui/assets/images/sync-status/warning-round.svg b/src/ui/assets/images/sync-status/warning-round.svg new file mode 100644 index 0000000..a7da081 --- /dev/null +++ b/src/ui/assets/images/sync-status/warning-round.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/ui/components/accounts/AccountsComponent.tsx b/src/ui/components/accounts/AccountsComponent.tsx index 8037403..99a49e2 100644 --- a/src/ui/components/accounts/AccountsComponent.tsx +++ b/src/ui/components/accounts/AccountsComponent.tsx @@ -17,7 +17,7 @@ type AccountsComponentProps = AppState; export function AccountsComponent ( { selectedAppodealAccount, - selectedAccount: {account: selectedAccount, logs}, + selectedAccount: {account: selectedAccount}, preferences: {accounts: {appodealAccounts}, multipleAccountsSupport}, syncHistory, syncProgress @@ -77,7 +77,6 @@ export function AccountsComponent ( appodealAccountId={appodealAccount.id} historyInfo={syncHistory[selectedAccount.id]} syncProgress={syncProgress[selectedAccount.id]} - logs={logs} /> :
Choose account
) diff --git a/src/ui/components/admob-account/AdmobAccountComponent.tsx b/src/ui/components/admob-account/AdmobAccountComponent.tsx index 907b9f6..f935077 100644 --- a/src/ui/components/admob-account/AdmobAccountComponent.tsx +++ b/src/ui/components/admob-account/AdmobAccountComponent.tsx @@ -5,7 +5,6 @@ import {SyncEventsTypes} from 'core/sync-apps/sync.events'; import {action, ActionTypes} from 'lib/actions'; import {getFormElement, singleEvent} from 'lib/dom'; import {sendToMain} from 'lib/messages'; -import {LogFileInfo} from 'lib/sync-logs/logger'; import {messageDialog} from 'lib/window'; import React, {Component} from 'react'; import {AccountStatusComponent} from 'ui/components/account-status/AccountStatusComponent'; @@ -19,7 +18,6 @@ interface AdmobAccountComponentProps { account: AdMobAccount; syncProgress: SyncProgress; historyInfo: SyncHistoryInfo; - logs: Array; } interface AdmobAccountComponentState { @@ -128,7 +126,7 @@ export class AdmobAccountComponent extends Component {this.isSetupFormVisible(account) &&
this.onFormInput()}>

Setup required

@@ -178,7 +176,10 @@ export class AdmobAccountComponent extends Component
} - + ; } } diff --git a/src/ui/components/log-list/LogList.scss b/src/ui/components/log-list/LogList.scss index d6e1554..a004323 100644 --- a/src/ui/components/log-list/LogList.scss +++ b/src/ui/components/log-list/LogList.scss @@ -5,20 +5,86 @@ overflow-y: auto; overflow-x: hidden; + > div { + display: block; + background-color: #212225; + + &:nth-child(2n) { + background-color: #2B2C2F; + } + } + &:not(:empty) { margin-block-start: 10px; } } .line { - max-width: calc(100% - 40px); - background-color: #212225; height: 43px; padding: 0 20px; justify-content: space-between; + align-items: center; display: flex; + &:hover { + background-color: #464646; + } + + .iconsGroup { + min-width: 80px; + justify-content: space-between; + align-items: center; + display: flex; + } + + .icon { + width: 25px; + height: 25px; + margin: 0 5px; + + img { + width: 25px; + height: 25px; + } + } + + @keyframes rotating { + to { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + from { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + .syncing { + animation: rotating 2s linear infinite; + } + + .count { + display: flex; + align-items: center; + justify-content: center; + background-color: gray; + text-align: center; + border-radius: 50%; + color: white; + + &:empty { + background-color: transparent; + } + } + + .time { width: 160px; margin-left: 10px; @@ -44,20 +110,33 @@ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + display: flex; } - &:nth-child(2n) { - background-color: #2B2C2F; + .open-button { + img { + max-height: 100%; + } } } +.applist { + user-select: text; + padding: 5px 5px 5px 25px; + + ul { + list-style: none; + padding: 0 10px; + } +} + +.toClipboardText { + user-select: all; +} + @media (min-width: 1050px) { .line { - width: 100%; - grid-template-areas: "time name actions"; - grid-template-columns: 170px auto auto; - .name { display: block; } diff --git a/src/ui/components/log-list/LogListComponent.tsx b/src/ui/components/log-list/LogListComponent.tsx index 3c0870a..c0c9062 100644 --- a/src/ui/components/log-list/LogListComponent.tsx +++ b/src/ui/components/log-list/LogListComponent.tsx @@ -1,16 +1,20 @@ import {AdMobAccount} from 'core/appdeal-api/interfaces/admob-account.interface'; -import {action, ActionTypes, LogAction} from 'lib/actions'; -import {singleEvent} from 'lib/dom'; +import {SyncHistoryInfo} from 'core/sync-apps/sync-history'; +import {AppInfo, SyncInfo} from 'core/sync-apps/sync-stats'; +import {SyncRunner} from 'core/sync-apps/sync.service'; +import {ActionTypes, LogAction} from 'lib/actions'; +import {classNames, singleEvent} from 'lib/dom'; import {sendToMain} from 'lib/messages'; -import {LogFileInfo} from 'lib/sync-logs/logger'; import React from 'react'; - +import Accordion from 'react-tiny-accordion'; +import Tooltip from 'react-tooltip-lite'; +import {TextToClipboard} from 'ui/components/text-to-clipboard/TextToClipboardComponent'; import style from './LogList.scss'; interface LogListComponentProps { + historyInfo: SyncHistoryInfo; admobAccount: AdMobAccount; - logs: LogFileInfo[]; appodealAccountId: string; } @@ -22,44 +26,120 @@ export class LogListComponent extends React.Component { return date.toLocaleString(); } - openLog (log: LogFileInfo) { + openLog (sync: SyncInfo) { return sendToMain('logs', { type: ActionTypes.openLogFile, payload: { account: this.props.admobAccount, - log + syncId: sync.id } } as LogAction); } - submitLogToAppodeal (log: LogFileInfo) { - return sendToMain('logs', action(ActionTypes.submitLogToAppodeal, { - account: this.props.admobAccount, - log, - appodealAccountId: this.props.appodealAccountId - })).then(() => { - alert(`Log has been sent to Appodeal. You can mention '${log.uuid}' in support ticket.`); + submitLogToAppodeal (sync: SyncInfo) { + return sendToMain('logs', { + type: ActionTypes.submitLogToAppodeal, + payload: { + account: this.props.admobAccount, + syncId: sync.id, + appodealAccountId: this.props.appodealAccountId + } + } as LogAction).then(() => { + alert(`Log has been sent to Appodeal. You can mention '${sync.id}' in support ticket.`); }).catch((e) => { console.error(e); - alert(`Failed to sent log, try again later.`); + alert(`Failed to sent log, ${e.errno === -2 ? 'file not found' : 'try again later.'}`); }); } + statusIcon (syncInfo: SyncInfo) { + if (!syncInfo.endTs) { + return + + ; + } + + if (syncInfo.terminated) { + return ; + } + + if (syncInfo.hasErrors) { + return ; + } + + return ; + } + + runnerIcon (syncInfo: SyncInfo) { + switch (syncInfo.runner) { + case SyncRunner.SyncScheduler: + return ; + case SyncRunner.User: + return ; + default: + return ''; + } + } + + affectedAppsCount (syncInfo: SyncInfo) { + const count = syncInfo.affectedApps.created.length + syncInfo.affectedApps.updated.length + syncInfo.affectedApps.deleted.length; + return count || ''; + } + + appList (title: string, list: AppInfo[]) { + if (!list.length) { + return ''; + } + return
+ {title}: +
    + {list.map(app =>
  • [{TextToClipboard({text: String(app.id)})}] {TextToClipboard({text: app.name})}
  • )} +
+
; + } + render (): React.ReactNode { - return
- {this.props.logs.map( - log =>
-
{LogListComponent.formatDate(log.ctime)}
-
{log.fileName}
-
- - + + return + {this.props.historyInfo.syncs.map( + syncInfo => +
+
+
+ {this.statusIcon(syncInfo)} +
+
+ {this.runnerIcon(syncInfo)} +
+ {this.affectedAppsCount(syncInfo) + ? Affected apps count
Click to see affected apps
)}> +
+ {this.affectedAppsCount(syncInfo)} +
+ + :
+ } +
+
{LogListComponent.formatDate(syncInfo.startTs)}
+
{syncInfo.id}
+
+ + +
+
+ } + > + {this.appList('Apps created', syncInfo.affectedApps.created)} + {this.appList('Apps updated', syncInfo.affectedApps.updated)} + {this.appList('Apps deleted', syncInfo.affectedApps.deleted)}
-
)} - ; + ; } } diff --git a/src/ui/components/text-to-clipboard/TextToClipboardComponent.tsx b/src/ui/components/text-to-clipboard/TextToClipboardComponent.tsx new file mode 100644 index 0000000..26bc6c9 --- /dev/null +++ b/src/ui/components/text-to-clipboard/TextToClipboardComponent.tsx @@ -0,0 +1,10 @@ +import {clipboard} from 'electron'; +import React from 'react'; + + +function toClipBoard (text) { + clipboard.writeText(text); + new Notification(`Copied to clipboard '${text}'`); +} + +export const TextToClipboard = ({text}: { text: string }) => ( toClipBoard(text)}>{text}); diff --git a/src/ui/style.scss b/src/ui/style.scss index 4c2ebaa..fa76d5e 100644 --- a/src/ui/style.scss +++ b/src/ui/style.scss @@ -212,5 +212,15 @@ } + .react-tooltip-lite { + border-radius: 4px; + background: #000; + color: white; + } + + .react-tooltip-lite-arrow { + border-color: #000; + } + }