diff --git a/frontend/app/mstore/integrationsStore.ts b/frontend/app/mstore/integrationsStore.ts new file mode 100644 index 000000000..52e42ceb8 --- /dev/null +++ b/frontend/app/mstore/integrationsStore.ts @@ -0,0 +1,126 @@ +import { makeAutoObservable } from 'mobx'; + +import { integrationsService } from 'App/services'; + +import { + Bugsnag, + Cloudwatch, + DatadogInt, + ElasticSearchInt, + GithubInt, + Integration, + IssueTracker, + JiraInt, + NewRelicInt, + RollbarInt, + SentryInt, + StackDriverInt, + SumoLogic, +} from './types/integrations'; + +class GenericIntegrationsStore { + list: any[] = []; + siteId: string | null = null; + constructor() { + makeAutoObservable(this); + } + + setSiteId(siteId: string) { + this.siteId = siteId; + } + + setList(list: any[]) { + this.list = list; + } + + fetchIntegrations = async () => { + //client.get(`/${siteID}/integrations`) + // this.setList() + }; +} + +class NamedIntegrationStore { + instance: T | null = null; + list: T[] = []; + fetched: boolean = false; + issuesFetched: boolean = false; + + constructor( + private readonly name: string, + private readonly NamedType: new (config: Record) => T + ) { + makeAutoObservable(this); + } + + setInstance(instance: T) { + this.instance = instance; + } + + setList(list: T[]) { + this.list = list; + } + + setFetched(fetched: boolean) { + this.fetched = fetched; + } + + setIssuesFetched(issuesFetched: boolean) { + this.issuesFetched = issuesFetched; + } + + fetchIntegrations = async () => { + const { data } = await integrationsService.fetchList(this.name); + this.setList( + data.map((config: Record) => new this.NamedType(config)) + ); + }; + + fetchIntegration = async (siteId: string) => { + const { data } = await integrationsService.fetchIntegration( + this.name, + siteId + ); + this.setInstance(new this.NamedType(data)); + }; + + saveIntegration(name: string, siteId: string) { + if (!this.instance) return; + const response = integrationsService.saveIntegration( + name, + siteId, + this.instance.toData() + ); + return; + } + + edit(data: T) { + this.setInstance(data); + } + + deleteIntegration(siteId: string) { + if (!this.instance) return; + integrationsService.removeIntegration(this.name, siteId); + } + + init(config: Record) { + this.instance = new this.NamedType(config); + } +} + +export class IntegrationsStore { + sentry = new NamedIntegrationStore('sentry', SentryInt); + datadog = new NamedIntegrationStore('datadog', DatadogInt); + stackdriver = new NamedIntegrationStore('stackdriver', StackDriverInt); + rollbar = new NamedIntegrationStore('rollbar', RollbarInt); + newrelic = new NamedIntegrationStore('newrelic', NewRelicInt); + bugsnag = new NamedIntegrationStore('bugsnag', Bugsnag); + cloudwatch = new NamedIntegrationStore('cloudwatch', Cloudwatch); + elasticsearch = new NamedIntegrationStore('elasticsearch', ElasticSearchInt); + sumologic = new NamedIntegrationStore('sumologic', SumoLogic); + jira = new NamedIntegrationStore('jira', JiraInt); + github = new NamedIntegrationStore('github', GithubInt); + issues = new NamedIntegrationStore('issues', IssueTracker); + integrations = new GenericIntegrationsStore(); + // + slack + // + teams +} diff --git a/frontend/app/mstore/types/integrations.ts b/frontend/app/mstore/types/integrations.ts new file mode 100644 index 000000000..3b3eec493 --- /dev/null +++ b/frontend/app/mstore/types/integrations.ts @@ -0,0 +1,444 @@ +import { makeAutoObservable } from 'mobx'; + +import { validateURL } from 'App/validate'; + +export interface Integration { + validate(): boolean; + exists(): boolean; + toData(): Record; +} + +export class SentryInt implements Integration { + projectId: number; + organizationSlug: string = ''; + projectSlug: string = ''; + token: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean(this.organizationSlug && this.projectSlug && this.token); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + organizationSlug: this.organizationSlug, + projectSlug: this.projectSlug, + token: this.token, + projectId: this.projectId, + }; + } +} + +export class DatadogInt implements Integration { + apiKey: string = ''; + applicationKey: string = ''; + projectId: number; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean(this.apiKey && this.applicationKey); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + apiKey: this.apiKey, + applicationKey: this.applicationKey, + projectId: this.projectId, + }; + } +} + +export class StackDriverInt implements Integration { + projectId: number; + logName: string = ''; + serviceAccountCredentials: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean( + this.serviceAccountCredentials !== '' && this.logName !== '' + ); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + logName: this.logName, + serviceAccountCredentials: this.serviceAccountCredentials, + projectId: this.projectId, + }; + } +} + +export class RollbarInt implements Integration { + projectId: number; + accessToken: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean(this.accessToken); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + accessToken: this.accessToken, + projectId: this.projectId, + }; + } +} + +export class NewRelicInt implements Integration { + projectId: number; + applicationId: string = ''; + xQueryKey: string = ''; + region: boolean = true; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean(this.applicationId && this.xQueryKey); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + applicationId: this.applicationId, + xQueryKey: this.xQueryKey, + region: this.region, + projectId: this.projectId, + }; + } +} + +export class Bugsnag implements Integration { + projectId: number; + authorizationToken: string = ''; + bugsnagProjectId: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean( + this.bugsnagProjectId !== '' && tokenRE.test(this.authorizationToken) + ); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + authorizationToken: this.authorizationToken, + bugsnagProjectId: this.bugsnagProjectId, + projectId: this.projectId, + }; + } +} + +export class Cloudwatch implements Integration { + projectId: number; + awsAccessKeyId: string = ''; + awsSecretAccessKey: string = ''; + region: string = 'us-east-1'; + logGroupName: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean( + this.awsAccessKeyId !== '' && + this.awsSecretAccessKey !== '' && + this.logGroupName !== '' && + this.region !== '' + ); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + awsAccessKeyId: this.awsAccessKeyId, + awsSecretAccessKey: this.awsSecretAccessKey, + region: this.region, + logGroupName: this.logGroupName, + projectId: this.projectId, + }; + } +} + +export class ElasticSearchInt implements Integration { + projectId: number; + host: string = ''; + apiKeyId: string = ''; + apiKey: string = ''; + indexes: string = '*log*'; + port: number = 9200; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + private validateKeys() { + return Boolean( + this.apiKeyId.length > API_KEY_ID_LENGTH && + this.apiKey.length > API_KEY_LENGTH && + validateURL(this.host) + ); + } + + validate() { + return ( + this.host !== '' && + this.apiKeyId !== '' && + this.apiKey !== '' && + this.indexes !== '' && + !!this.port && + this.validateKeys() + ); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + host: this.host, + apiKeyId: this.apiKeyId, + apiKey: this.apiKey, + indexes: this.indexes, + port: this.port, + projectId: this.projectId, + }; + } +} + +export class SumoLogic implements Integration { + projectId: number; + accessId: string = ''; + accessKey: string = ''; + region: 'au'; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return Boolean(this.accessKey && this.accessId); + } + + exists() { + return this.projectId >= 0; + } + + toData() { + return { + accessId: this.accessId, + accessKey: this.accessKey, + region: this.region, + projectId: this.projectId, + }; + } +} + +export class JiraInt implements Integration { + projectId: number; + username: string = ''; + token: string = ''; + url: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validateFetchProjects() { + return this.username !== '' && this.token !== '' && validateURL(this.url); + } + + validate() { + return this.validateFetchProjects(); + } + + exists() { + return !!this.token; + } + + toData() { + return { + username: this.username, + token: this.token, + url: this.url, + projectId: this.projectId, + }; + } +} + +export class GithubInt implements Integration { + projectId: number; + provider: string = 'github'; + token: string = ''; + + constructor(config: any) { + Object.assign(this, { + ...config, + projectId: config.projectId || -1, + }); + } + + validate() { + return this.token !== ''; + } + + exists() { + return !!this.token; + } + + toData() { + return { + provider: this.provider, + token: this.token, + projectId: this.projectId, + }; + } +} + +export class IssueTracker implements Integration { + username: string = ''; + token: string = ''; + url: string = ''; + provider = 'jira'; + + constructor(config: any) { + Object.assign(this, { + ...config, + }); + } + + validateFetchProjects() { + return this.username !== '' && this.token !== '' && validateURL(this.url); + } + + validate() { + return !!this.url; + } + + exists() { + return !!this.token; + } + + toData() { + return { + username: this.username, + token: this.token, + url: this.url, + provider: this.provider, + }; + } +} + +export const sumoRegionLabels = { + au: 'Asia Pacific (Sydney)', + ca: 'Canada (Central)', + de: 'EU (Frankfurt)', + eu: 'EU (Ireland)', + fed: 'US East (N. Virginia)', + in: 'Asia Pacific (Mumbai)', + jp: 'Asia Pacific (Tokyo)', + us1: 'US East (N. Virginia)', + us2: 'US West (Oregon)', +}; +export const API_KEY_ID_LENGTH = 5; +export const API_KEY_LENGTH = 5; +export const SECRET_ACCESS_KEY_LENGTH = 40; +export const ACCESS_KEY_ID_LENGTH = 20; +export const tokenRE = + /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i; +export const awsRegionLabels = { + 'us-east-1': 'US East (N. Virginia)', + 'us-east-2': 'US East (Ohio)', + 'us-west-1': 'US West (N. California)', + 'us-west-2': 'US West (Oregon)', + 'ap-east-1': 'Asia Pacific (Hong Kong)', + 'ap-south-1': 'Asia Pacific (Mumbai)', + 'ap-northeast-2': 'Asia Pacific (Seoul)', + 'ap-southeast-1': 'Asia Pacific (Singapore)', + 'ap-southeast-2': 'Asia Pacific (Sydney)', + 'ap-northeast-1': 'Asia Pacific (Tokyo)', + 'ca-central-1': 'Canada (Central)', + 'eu-central-1': 'EU (Frankfurt)', + 'eu-west-1': 'EU (Ireland)', + 'eu-west-2': 'EU (London)', + 'eu-west-3': 'EU (Paris)', + 'eu-north-1': 'EU (Stockholm)', + 'me-south-1': 'Middle East (Bahrain)', + 'sa-east-1': 'South America (São Paulo)', +}; diff --git a/frontend/app/services/IntegrationsService.ts b/frontend/app/services/IntegrationsService.ts new file mode 100644 index 000000000..be23ee449 --- /dev/null +++ b/frontend/app/services/IntegrationsService.ts @@ -0,0 +1,33 @@ +import BaseService from "./BaseService"; + +export default class IntegrationsService extends BaseService { + fetchList = async (name) => { + const r = await this.client.get(`/integrations/${name}`) + const data = await r.json() + + return data + } + + fetchIntegration = async (name: string, siteId: string) => { + const url = siteId && name !== 'github' && name !== 'jira' ? `/${siteId}/integrations/${name}` : `/integrations/${name}` + const r = await this.client.get(url) + const data = await r.json() + + return data + } + + saveIntegration = async (name: string, siteId: string, data: any) => { + const url = (siteId ? `/${siteId}` : '') + `/integrations/${name}` + const r = await this.client.post(url, data) + const res = await r.json() + + return res + } + + removeIntegration = async (name: string, siteId: string) => { + const url = (siteId ? `/${siteId}` : '') + `/integrations/${name}` + const r = await this.client.delete(url) + const res = await r.json() + + return res +} diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 4a6e743d1..a6bf3062b 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -21,6 +21,7 @@ import SpotService from './spotService'; import LoginService from "./loginService"; import FilterService from "./FilterService"; import IssueReportsService from "./IssueReportsService"; +import IntegrationsService from './IntegrationsService'; export const dashboardService = new DashboardService(); export const metricService = new MetricService(); @@ -44,6 +45,7 @@ export const spotService = new SpotService(); export const loginService = new LoginService(); export const filterService = new FilterService(); export const issueReportsService = new IssueReportsService(); +export const integrationsService = new IntegrationsService(); export const services = [ dashboardService, @@ -68,4 +70,5 @@ export const services = [ loginService, filterService, issueReportsService, + integrationsService, ];