4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 |
6 | import type { Plugin } from 'chart.js';
7 |
8 | declare module 'chart.js' {
9 | interface ChartDatasetProperties<_TType extends ChartType, _TData> {
10 | trendlineLinear?: TrendlineLinearPlugin.TrendlineLinearOptions;
11 | }
12 | }
13 |
14 | declare namespace TrendlineLinearPlugin {
15 | interface TrendlineLinearOptions {
16 | colorMin: 'string';
17 | colorMax: 'string';
18 | lineStyle: 'dotted' | 'solid';
19 | width: number;
20 | projection?: boolean;
21 | }
22 | }
23 |
24 | declare const TrendlineLinearPlugin: Plugin;
25 |
26 | export = TrendlineLinearPlugin;
27 | export as namespace TrendlineLinearPlugin;
28 |
--------------------------------------------------------------------------------
/web-server/libdefs/font.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.ttf' {
2 | const content: any;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/web-server/libdefs/img.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: any;
3 | export default content;
4 | }
5 | declare module '*.png' {
6 | const content: any;
7 | export default content;
8 | }
9 |
--------------------------------------------------------------------------------
/web-server/libdefs/intl.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace Intl {
2 | type Key =
3 | | 'calendar'
4 | | 'collation'
5 | | 'currency'
6 | | 'numberingSystem'
7 | | 'timeZone'
8 | | 'unit';
9 |
10 | function supportedValuesOf(input: Key): string[];
11 | }
12 |
--------------------------------------------------------------------------------
/web-server/libdefs/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unused-imports/no-unused-imports */
2 | import NextAuth from 'next-auth';
3 |
4 | declare module 'next-auth' {
5 | /**
6 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
7 | */
8 | interface Session {
9 | org: Org;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/web-server/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | import { getFeaturesFromReq } from '@/api-helpers/features';
4 | import { defaultFlags } from '@/constants/feature';
5 |
6 | export async function middleware(request: NextRequest) {
7 | const flagOverrides = getFeaturesFromReq(request as any);
8 | const flags = { ...defaultFlags, ...flagOverrides };
9 |
10 | const url = request.nextUrl.clone();
11 |
12 | // Forward as-is if it's a next-auth URL, except /session
13 | if (
14 | url.pathname.startsWith('/api/auth') &&
15 | !url.pathname.startsWith('/api/auth/session')
16 | ) {
17 | return NextResponse.next();
18 | }
19 |
20 | url.searchParams.append('feature_flags', JSON.stringify(flags));
21 |
22 | return NextResponse.rewrite(url);
23 | }
24 |
25 | export const config = {
26 | matcher: [
27 | '/api/auth/:path*',
28 | '/api/integrations/:path*',
29 | '/api/internal/:path*',
30 | '/api/resources/:path*'
31 | ]
32 | };
33 |
--------------------------------------------------------------------------------
/web-server/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/web-server/next.d.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | NextComponentType,
3 | NextPageContext
4 | } from 'next/dist/shared/lib/utils';
5 | import type { ReactElement, ReactNode } from 'react';
6 |
7 | declare module 'next' {
8 | export declare type NextPage = NextComponentType<
9 | NextPageContext,
10 | IP,
11 | P
12 | > & {
13 | getLayout?: (page: ReactElement) => ReactNode;
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/web-server/pages/api/db_status.ts:
--------------------------------------------------------------------------------
1 | import { Endpoint, nullSchema } from '@/api-helpers/global';
2 | import { db, getCountFromQuery } from '@/utils/db';
3 |
4 | const endpoint = new Endpoint(nullSchema, { unauthenticated: true });
5 |
6 | endpoint.handle.GET(nullSchema, async (_req, res) => {
7 | const lastBuildDate =
8 | process.env.NEXT_PUBLIC_BUILD_TIME &&
9 | new Date(process.env.NEXT_PUBLIC_BUILD_TIME);
10 |
11 | const start = new Date();
12 | const dataCheck = await db('Organization').count('*').then(getCountFromQuery);
13 | const diff = new Date().getTime() - start.getTime();
14 |
15 | res.send({
16 | status: 'OK',
17 | environment: process.env.NEXT_PUBLIC_APP_ENVIRONMENT,
18 | build_time: lastBuildDate,
19 | data_check: dataCheck,
20 | latency: diff
21 | });
22 | });
23 |
24 | export default endpoint.serve();
25 |
--------------------------------------------------------------------------------
/web-server/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 |
5 | const getSchema = yup.object().shape({
6 | name: yup.string().optional()
7 | });
8 |
9 | const endpoint = new Endpoint(nullSchema, { unauthenticated: true });
10 |
11 | endpoint.handle.GET(getSchema, async (req, res) => {
12 | const { name } = req.payload;
13 |
14 | res
15 | .status(200)
16 | .send(name ? { hello: name } : { message: 'Usage: ?name=' });
17 | });
18 |
19 | export default endpoint.serve();
20 |
--------------------------------------------------------------------------------
/web-server/pages/api/integrations/github/selected.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 | import { Columns, Table } from '@/constants/db';
5 | import { selectedDBReposMock } from '@/mocks/github';
6 | import { db } from '@/utils/db';
7 |
8 | const getSchema = yup.object().shape({
9 | org_id: yup.string().uuid().required()
10 | });
11 |
12 | const endpoint = new Endpoint(nullSchema);
13 |
14 | endpoint.handle.GET(getSchema, async (req, res) => {
15 | if (req.meta?.features?.use_mock_data) {
16 | return res.send(selectedDBReposMock);
17 | }
18 |
19 | const { org_id } = req.payload;
20 |
21 | const data = await db(Table.OrgRepo)
22 | .select('*')
23 | .where({ org_id, provider: 'github' })
24 | .andWhereNot(Columns[Table.OrgRepo].is_active, false);
25 |
26 | res.send(data);
27 | });
28 |
29 | export default endpoint.serve();
30 |
--------------------------------------------------------------------------------
/web-server/pages/api/integrations/index.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 | import { Columns, Table } from '@/constants/db';
5 | import { Integration } from '@/constants/integrations';
6 | import { db, getFirstRow } from '@/utils/db';
7 |
8 | const getSchema = yup.object().shape({
9 | user_id: yup.string().uuid().required()
10 | });
11 |
12 | const deleteSchema = yup.object().shape({
13 | provider: yup.string().oneOf(Object.values(Integration)),
14 | org_id: yup.string().uuid().required()
15 | });
16 |
17 | const endpoint = new Endpoint(nullSchema);
18 |
19 | endpoint.handle.GET(getSchema, async (req, res) => {
20 | const data = await db(Table.UserIdentity)
21 | .select(Columns[Table.UserIdentity].provider)
22 | .where(Columns[Table.UserIdentity].user_id, req.payload.user_id);
23 |
24 | res.send({ integrations: data });
25 | });
26 |
27 | endpoint.handle.DELETE(deleteSchema, async (req, res) => {
28 | const data = await db(Table.Integration)
29 | .delete()
30 | .where({
31 | [Columns[Table.Integration].org_id]: req.payload.org_id,
32 | [Columns[Table.Integration].name]: req.payload.provider
33 | })
34 | .returning('*')
35 | .then(getFirstRow);
36 |
37 | res.send(data);
38 | });
39 |
40 | export default endpoint.serve();
41 |
--------------------------------------------------------------------------------
/web-server/pages/api/integrations/integrations-map.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { getLastSyncedAtForCodeProvider } from '@/api/internal/[org_id]/sync_repos';
4 | import { Endpoint, nullSchema } from '@/api-helpers/global';
5 | import { CODE_PROVIDER_INTEGRATIONS_MAP } from '@/constants/api';
6 |
7 | import { getOrgIntegrations } from '../auth/session';
8 |
9 | const endpoint = new Endpoint(nullSchema);
10 |
11 | const getSchema = yup.object().shape({
12 | org_id: yup.string().required()
13 | });
14 |
15 | endpoint.handle.GET(getSchema, async (req, res) => {
16 | const { org_id } = req.payload;
17 | const [integrationsLinkedAtMap, codeProviderLastSyncedAt] = await Promise.all(
18 | [getOrgIntegrations(), getLastSyncedAtForCodeProvider(org_id)]
19 | );
20 | const integrations = {} as IntegrationsMap;
21 | Object.entries(integrationsLinkedAtMap).forEach(
22 | ([integrationName, integrationLinkedAt]) => {
23 | integrations[integrationName as keyof IntegrationsMap] = {
24 | integrated: true,
25 | linked_at: integrationLinkedAt,
26 | last_synced_at: CODE_PROVIDER_INTEGRATIONS_MAP[
27 | integrationName as keyof typeof CODE_PROVIDER_INTEGRATIONS_MAP
28 | ]
29 | ? codeProviderLastSyncedAt
30 | : null
31 | };
32 | }
33 | );
34 | res.send(integrations);
35 | });
36 |
37 | export default endpoint.serve();
38 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/[org_id]/incident_services.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { handleRequest } from '@/api-helpers/axios';
4 | import { Endpoint, nullSchema } from '@/api-helpers/global';
5 | import { OrgIncidentServicesApiResponse } from '@/types/resources';
6 |
7 | const pathSchema = yup.object().shape({
8 | org_id: yup.string().uuid().required()
9 | });
10 |
11 | const endpoint = new Endpoint(pathSchema);
12 |
13 | endpoint.handle.GET(nullSchema, async (req, res) => {
14 | const { org_id } = req.payload;
15 | return res.send(getSelectedIncidentServices(org_id));
16 | });
17 |
18 | export const getSelectedIncidentServices = (org_id: ID) =>
19 | handleRequest(
20 | `/orgs/${org_id}/incident_services`
21 | ).then((r) => r.incident_services);
22 |
23 | export default endpoint.serve();
24 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/[org_id]/integrations/incident_providers.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { handleRequest } from '@/api-helpers/axios';
4 | import { Endpoint } from '@/api-helpers/global';
5 |
6 | type OrgIncidentProviderType = { incident_providers: string[] };
7 |
8 | const pathSchema = yup.object().shape({
9 | org_id: yup.string().uuid().required()
10 | });
11 |
12 | const endpoint = new Endpoint(pathSchema);
13 |
14 | endpoint.handle.GET(null, async (req, res) => {
15 | const { org_id } = req.payload;
16 | return res.send(await getOrgIncidentsProviders(org_id));
17 | });
18 |
19 | export const getOrgIncidentsProviders = async (org_id: ID) => {
20 | return await handleRequest(
21 | `/orgs/${org_id}/incident_providers`
22 | );
23 | };
24 |
25 | export default endpoint.serve();
26 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/ai/models.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { handleRequest } from '@/api-helpers/axios';
4 | import { Endpoint, nullSchema } from '@/api-helpers/global';
5 |
6 | const getSchema = yup.object().shape({});
7 |
8 | const endpoint = new Endpoint(nullSchema);
9 |
10 | endpoint.handle.GET(getSchema, async (_req, res) => {
11 | const response = await handleRequest('ai/models', { method: 'GET' });
12 | res.send(Object.keys(response));
13 | });
14 |
15 | export default endpoint.serve();
16 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/deployments/prs.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { mockDeploymentPrs } from '@/api/internal/team/[team_id]/deployment_prs';
4 | import { handleRequest } from '@/api-helpers/axios';
5 | import { Endpoint, nullSchema } from '@/api-helpers/global';
6 | import { adaptPr } from '@/api-helpers/pr';
7 | import { BasePR } from '@/types/resources';
8 |
9 | const getSchema = yup.object().shape({
10 | deployment_id: yup.string().required()
11 | });
12 |
13 | const endpoint = new Endpoint(nullSchema);
14 |
15 | endpoint.handle.GET(getSchema, async (req, res) => {
16 | if (req.meta?.features?.use_mock_data) return res.send(mockDeploymentPrs);
17 |
18 | const { deployment_id } = req.payload;
19 | return res.send(
20 | await handleRequest<{ data: BasePR[]; total_count: number }>(
21 | `/deployments/${deployment_id}/prs`
22 | ).then((r) => r.data.map(adaptPr))
23 | );
24 | });
25 |
26 | export default endpoint.serve();
27 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/hello.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { handleRequest } from '@/api-helpers/axios';
4 | import { Endpoint, nullSchema } from '@/api-helpers/global';
5 |
6 | const getSchema = yup.object().shape({
7 | log_text: yup.string().required()
8 | });
9 |
10 | const endpoint = new Endpoint(nullSchema);
11 |
12 | // @ts-ignore
13 | endpoint.handle.GET(getSchema, async (req, res) => {
14 | return res.send(await handleRequest(`/hello`));
15 | });
16 |
17 | export default endpoint.serve();
18 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/team/[team_id]/deployment_freq.ts:
--------------------------------------------------------------------------------
1 | import { handleRequest } from '@/api-helpers/axios';
2 | import {
3 | TeamDeploymentsConfigured,
4 | RepoWithSingleWorkflow
5 | } from '@/types/resources';
6 |
7 | export const fetchWorkflowConfiguredRepos = async (team_id: ID) => {
8 | const [assignedReposConfig, workflowConfiguration] = await Promise.all([
9 | handleRequest<{
10 | repos_included: RepoWithSingleWorkflow[];
11 | all_team_repos: RepoWithSingleWorkflow[];
12 | }>(`/teams/${team_id}/lead_time/repos`).catch((e) => {
13 | console.error(e);
14 | return {
15 | repos_included: [],
16 | all_team_repos: []
17 | };
18 | }),
19 | handleRequest(
20 | `/teams/${team_id}/deployments_configured`
21 | )
22 | ]);
23 | return {
24 | ...assignedReposConfig,
25 | ...workflowConfiguration
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/team/[team_id]/deployment_prs.ts:
--------------------------------------------------------------------------------
1 | export const mockDeploymentPrs = [
2 | {
3 | number: '129',
4 | title: 'Fix login issues',
5 | state: 'MERGED',
6 | first_response_time: 1070,
7 | rework_time: 0,
8 | merge_time: 302,
9 | cycle_time: 1372,
10 | author: {
11 | username: 'shivam-bit',
12 | linked_user: {
13 | id: 'd3731fae-68e9-4ef5-92fb-a7cfb228b888',
14 | name: 'Shivam Singh',
15 | email: 'shivam@middlewarehq.com'
16 | }
17 | },
18 | reviewers: [
19 | {
20 | username: 'jayantbh'
21 | }
22 | ],
23 | repo_name: 'web-manager-dash',
24 | pr_link: 'https://github.com/monoclehq/web-manager-dash/pull/129',
25 | base_branch: 'main',
26 | head_branch: 'GROW-336',
27 | created_at: '2023-04-03T10:45:41+00:00',
28 | updated_at: '2023-04-03T11:08:33+00:00',
29 | state_changed_at: '2023-04-03T11:08:33+00:00',
30 | commits: 2,
31 | additions: 95,
32 | deletions: 30,
33 | changed_files: 8,
34 | comments: 1,
35 | provider: 'github',
36 | rework_cycles: 0
37 | }
38 | ];
39 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/team/[team_id]/revert_prs.ts:
--------------------------------------------------------------------------------
1 | import { batchPaginatedRequest } from '@/api-helpers/internal';
2 | import { updatePrFilterParams } from '@/api-helpers/team';
3 | import { RevertedAndOriginalPrPair } from '@/types/resources';
4 |
5 | export const getTeamRevertedPrs = async (
6 | params: Awaited> & {
7 | from_time: string;
8 | to_time: string;
9 | team_id: ID;
10 | }
11 | ) => {
12 | const { team_id, from_time, to_time, pr_filter } = params;
13 |
14 | const response = await batchPaginatedRequest(
15 | `/teams/${team_id}/revert_prs`,
16 | {
17 | page: 1,
18 | page_size: 100,
19 | ...{
20 | from_time,
21 | to_time,
22 | pr_filter
23 | }
24 | }
25 | ).then((r) => r.data);
26 | return adaptRevertedPrs(response);
27 | };
28 |
29 | const adaptRevertedPrs = (revertedPrsSetArray: RevertedAndOriginalPrPair[]) =>
30 | revertedPrsSetArray.map((revertedPrsSet) => {
31 | revertedPrsSet.revert_pr.original_reverted_pr =
32 | revertedPrsSet.original_reverted_pr;
33 | return revertedPrsSet.revert_pr;
34 | });
35 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/team/[team_id]/settings.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { handleRequest } from '@/api-helpers/axios';
4 | import { Endpoint } from '@/api-helpers/global';
5 | import { FetchTeamSettingsAPIResponse } from '@/types/resources';
6 |
7 | const pathSchema = yup.object().shape({
8 | team_id: yup.string().uuid().required()
9 | });
10 | const getSchema = yup.object().shape({
11 | setting_type: yup.string().required()
12 | });
13 |
14 | const putSchema = yup.object().shape({
15 | setting_type: yup.string().required(),
16 | setting_data: yup.object()
17 | });
18 |
19 | const endpoint = new Endpoint(pathSchema);
20 |
21 | endpoint.handle.PUT(putSchema, async (req, res) => {
22 | const { team_id, setting_data, setting_type } = req.payload;
23 | return res.send(
24 | await handleRequest(
25 | `/teams/${team_id}/settings`,
26 | {
27 | method: 'PUT',
28 | data: {
29 | setting_type,
30 | setting_data
31 | }
32 | }
33 | )
34 | );
35 | });
36 |
37 | endpoint.handle.GET(getSchema, async (req, res) => {
38 | const { team_id, setting_type } = req.payload;
39 | return res.send(
40 | await handleRequest(
41 | `/teams/${team_id}/settings`,
42 | {
43 | params: {
44 | setting_type
45 | }
46 | }
47 | )
48 | );
49 | });
50 |
51 | export default endpoint.serve();
52 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal/track.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 |
5 | const postSchema = yup.object().shape({});
6 |
7 | const endpoint = new Endpoint(nullSchema);
8 |
9 | endpoint.handle.POST(postSchema, async (_req, res) => {
10 | res.send({ success: true });
11 | });
12 |
13 | export default endpoint.serve();
14 |
--------------------------------------------------------------------------------
/web-server/pages/api/internal_status.ts:
--------------------------------------------------------------------------------
1 | import { handleRequest } from '@/api-helpers/axios';
2 | import { Endpoint, nullSchema } from '@/api-helpers/global';
3 |
4 | const endpoint = new Endpoint(nullSchema, { unauthenticated: true });
5 |
6 | endpoint.handle.GET(nullSchema, async (_req, res) => {
7 | const lastBuildDate =
8 | process.env.NEXT_PUBLIC_BUILD_TIME &&
9 | new Date(process.env.NEXT_PUBLIC_BUILD_TIME);
10 |
11 | const start = new Date();
12 | const dataCheck = await handleRequest('/');
13 | const diff = new Date().getTime() - start.getTime();
14 |
15 | res.send({
16 | status: 'OK',
17 | environment: process.env.NEXT_PUBLIC_APP_ENVIRONMENT,
18 | build_time: lastBuildDate,
19 | data_check: dataCheck,
20 | latency: diff
21 | });
22 | });
23 |
24 | export default endpoint.serve();
25 |
--------------------------------------------------------------------------------
/web-server/pages/api/resources/orgs/[org_id]/filter_users.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint } from '@/api-helpers/global';
4 | import { Table } from '@/constants/db';
5 | import { db } from '@/utils/db';
6 |
7 | const getSchema = yup.object().shape({
8 | user_id: yup.string().uuid().required(),
9 | type: yup.string().required()
10 | });
11 |
12 | const pathnameSchema = yup.object().shape({
13 | org_id: yup.string().uuid().required()
14 | });
15 |
16 | const endpoint = new Endpoint(pathnameSchema);
17 |
18 | endpoint.handle.GET(getSchema, async (req, res) => {
19 | if (req.meta?.features?.use_mock_data) {
20 | return res.send([]);
21 | }
22 |
23 | const { org_id, user_id } = req.payload;
24 |
25 | const allRelations = await db('TeamRelations').select('*').where({ org_id });
26 |
27 | const usersAsManagers = allRelations.map((relation) => relation.user_id);
28 | const usersAsDirects = allRelations.map(
29 | (relation) => relation.related_user_id
30 | );
31 |
32 | const usersToExclude = Array.from(
33 | new Set([...usersAsManagers, ...usersAsDirects, user_id])
34 | );
35 |
36 | const data = await db(Table.Users)
37 | .select('*')
38 | .where({ org_id })
39 | .and.not.whereIn('id', usersToExclude);
40 |
41 | res.send(data);
42 | });
43 |
44 | export default endpoint.serve();
45 |
--------------------------------------------------------------------------------
/web-server/pages/api/resources/orgs/[org_id]/repos.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 | import { teamReposMock } from '@/mocks/repos';
5 | import { db } from '@/utils/db';
6 |
7 | const pathSchema = yup.object().shape({
8 | org_id: yup.string().uuid().required()
9 | });
10 |
11 | const endpoint = new Endpoint(pathSchema);
12 |
13 | endpoint.handle.GET(nullSchema, async (req, res) => {
14 | if (req.meta?.features?.use_mock_data) {
15 | return res.send(teamReposMock);
16 | }
17 |
18 | const data = await db('OrgRepo')
19 | .select('*')
20 | .where('is_active', true)
21 | .andWhere('org_id', req.payload.org_id);
22 |
23 | res.send(data);
24 | });
25 |
26 | export default endpoint.serve();
27 |
--------------------------------------------------------------------------------
/web-server/pages/api/resources/orgs/[org_id]/teams/team_branch_map.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import {
4 | getAllTeamsReposProdBranchesForOrg,
5 | transformTeamRepoBranchesToMap
6 | } from '@/api/internal/team/[team_id]/repo_branches';
7 | import { Endpoint } from '@/api-helpers/global';
8 | import { getTeamV2Mock } from '@/mocks/teams';
9 |
10 | const getSchema = yup.object().shape({});
11 |
12 | const pathnameSchema = yup.object().shape({
13 | org_id: yup.string().uuid().required()
14 | });
15 |
16 | const endpoint = new Endpoint(pathnameSchema);
17 |
18 | endpoint.handle.GET(getSchema, async (req, res) => {
19 | if (req.meta?.features?.use_mock_data) {
20 | return res.send(getTeamV2Mock['teamReposProdBranchMap']);
21 | }
22 |
23 | const { org_id } = req.payload;
24 |
25 | const teamsReposProductionBranchDetails =
26 | await getAllTeamsReposProdBranchesForOrg(org_id);
27 |
28 | const teamReposProdBranchMap = transformTeamRepoBranchesToMap(
29 | teamsReposProductionBranchDetails
30 | );
31 |
32 | res.send({ teamReposProdBranchMap });
33 | });
34 |
35 | export default endpoint.serve();
36 |
--------------------------------------------------------------------------------
/web-server/pages/api/resources/search/teams.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 | import { Columns, Table } from '@/constants/db';
5 | import { db } from '@/utils/db';
6 |
7 | const getSchema = yup.object().shape({
8 | name: yup.string().optional().nullable(),
9 | org_id: yup.string().uuid().required()
10 | });
11 |
12 | const endpoint = new Endpoint(nullSchema);
13 |
14 | endpoint.handle.GET(getSchema, async (req, res) => {
15 | const { name, org_id } = req.payload;
16 |
17 | const query = db('Team')
18 | .select('*')
19 | .where(Columns[Table.Team].org_id, org_id)
20 | .andWhere(Columns[Table.Team].is_deleted, false);
21 |
22 | if (!name) return res.send(await query);
23 | res.send(await query.whereILike('name', `%${name}%`));
24 | });
25 |
26 | export default endpoint.serve();
27 |
--------------------------------------------------------------------------------
/web-server/pages/api/resources/search/user.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { Endpoint, nullSchema } from '@/api-helpers/global';
4 | import { Columns, Table } from '@/constants/db';
5 | import { db } from '@/utils/db';
6 |
7 | const getSchema = yup.object().shape({
8 | name: yup.string().optional().nullable(),
9 | org_id: yup.string().uuid().required(),
10 | exclude_users: yup.array().of(yup.string().uuid()).optional()
11 | });
12 |
13 | const endpoint = new Endpoint(nullSchema);
14 |
15 | endpoint.handle.GET(getSchema, async (req, res) => {
16 | const { name, org_id } = req.payload;
17 |
18 | const query = db(Table.Users)
19 | .select('*')
20 | .where(Columns[Table.Users].org_id, org_id)
21 | .andWhere(Columns[Table.Team].is_deleted, false);
22 |
23 | if (req.payload.exclude_users?.length) {
24 | query.whereNotIn('id', req.payload.exclude_users);
25 | }
26 | if (!name) return res.send(await query);
27 | res.send(await query.whereILike('name', `%${name}%`));
28 | });
29 |
30 | export default endpoint.serve();
31 |
--------------------------------------------------------------------------------
/web-server/pages/api/resources/teams/[team_id]/unsynced_repos.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | import { getTeamRepos } from '@/api/resources/team_repos';
4 | import { Endpoint, nullSchema } from '@/api-helpers/global';
5 | import { Table } from '@/constants/db';
6 | import { uuid } from '@/utils/datatype';
7 | import { db } from '@/utils/db';
8 |
9 | const pathSchema = yup.object().shape({
10 | team_id: yup.string().uuid().required()
11 | });
12 |
13 | const endpoint = new Endpoint(pathSchema);
14 |
15 | endpoint.handle.GET(nullSchema, async (req, res) => {
16 | if (req.meta?.features?.use_mock_data) {
17 | return res.send([uuid(), uuid()]);
18 | }
19 |
20 | res.send(await getUnsyncedRepos(req.payload.team_id));
21 | });
22 |
23 | export const getUnsyncedRepos = async (teamId: ID) => {
24 | const query = db(Table.Bookmark).select('repo_id');
25 |
26 | const teamRepoIds = await getTeamRepos(teamId).then((res) =>
27 | res.map((repo) => repo.id)
28 | );
29 |
30 | const syncedRepos = (await query
31 | .whereIn('repo_id', teamRepoIds)
32 | .then((res) => res.map((item) => item?.repo_id))) as ID[];
33 |
34 | const unsyncedRepos = teamRepoIds.filter(
35 | (repo) => !syncedRepos.includes(repo)
36 | );
37 |
38 | return unsyncedRepos;
39 | };
40 |
41 | export default endpoint.serve();
42 |
--------------------------------------------------------------------------------
/web-server/pages/api/status.ts:
--------------------------------------------------------------------------------
1 | import { isBefore } from 'date-fns';
2 | import * as yup from 'yup';
3 |
4 | import { Endpoint, nullSchema } from '@/api-helpers/global';
5 |
6 | const endpoint = new Endpoint(nullSchema, { unauthenticated: true });
7 |
8 | const getSchema = yup.object().shape({
9 | build_time: yup.date().optional()
10 | });
11 |
12 | endpoint.handle.GET(getSchema, async (req, res) => {
13 | const { build_time } = req.payload;
14 |
15 | const lastBuildDate =
16 | process.env.NEXT_PUBLIC_BUILD_TIME &&
17 | new Date(process.env.NEXT_PUBLIC_BUILD_TIME);
18 |
19 | const newBuildReady =
20 | lastBuildDate &&
21 | build_time &&
22 | isBefore(new Date(build_time), lastBuildDate);
23 |
24 | res.send({
25 | status: 'OK',
26 | environment: process.env.NEXT_PUBLIC_APP_ENVIRONMENT,
27 | build_time: lastBuildDate,
28 | new_build_available: newBuildReady
29 | });
30 | });
31 |
32 | export default endpoint.serve();
33 |
--------------------------------------------------------------------------------
/web-server/pages/dora-metrics/index.tsx:
--------------------------------------------------------------------------------
1 | import ExtendedSidebarLayout from 'src/layouts/ExtendedSidebarLayout';
2 |
3 | import { Authenticated } from '@/components/Authenticated';
4 | import { FlexBox } from '@/components/FlexBox';
5 | import Loader from '@/components/Loader';
6 | import { FetchState } from '@/constants/ui-states';
7 | import { useRedirectWithSession } from '@/constants/useRoute';
8 | import { DoraMetricsBody } from '@/content/DoraMetrics/DoraMetricsBody';
9 | import { PageWrapper } from '@/content/PullRequests/PageWrapper';
10 | import { useAuth } from '@/hooks/useAuth';
11 | import { useSelector } from '@/store';
12 | import { PageLayout } from '@/types/resources';
13 | function Page() {
14 | useRedirectWithSession();
15 | const isLoading = useSelector(
16 | (s) => s.doraMetrics.requests?.metrics_summary === FetchState.REQUEST
17 | );
18 | const { integrationList } = useAuth();
19 |
20 | return (
21 |
24 | DORA metrics
25 |
26 | }
27 | pageTitle="DORA metrics"
28 | isLoading={isLoading}
29 | teamDateSelectorMode="single"
30 | >
31 | {integrationList.length > 0 ? : }
32 |
33 | );
34 | }
35 |
36 | Page.getLayout = (page: PageLayout) => (
37 |
38 | {page}
39 |
40 | );
41 |
42 | export default Page;
43 |
--------------------------------------------------------------------------------
/web-server/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, LinearProgress, styled } from '@mui/material';
2 | import Head from 'next/head';
3 | import { ReactElement } from 'react';
4 | import { Authenticated } from 'src/components/Authenticated';
5 | import ExtendedSidebarLayout from 'src/layouts/ExtendedSidebarLayout';
6 |
7 | import { FlexBox } from '@/components/FlexBox';
8 | import { Line } from '@/components/Text';
9 | import { useRedirectWithSession } from '@/constants/useRoute';
10 |
11 | const OverviewWrapper = styled(Box)(
12 | ({ theme }) => `
13 | overflow: auto;
14 | background: ${theme.palette.common.white};
15 | flex: 1;
16 | overflow-x: hidden;
17 | `
18 | );
19 |
20 | function Overview() {
21 | useRedirectWithSession();
22 |
23 | return (
24 |
25 |
26 | MiddlewareHQ
27 |
28 |
29 |
30 | Please wait while we load the session for you...
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default Overview;
39 |
40 | Overview.getLayout = function getLayout(page: ReactElement) {
41 | return (
42 |
43 | {page}
44 |
45 | );
46 | };
47 |
48 | // Overview.getInitialProps = redirectPage(DEFAULT_HOME_ROUTE.PATH);
49 |
--------------------------------------------------------------------------------
/web-server/pages/server-admin.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { Authenticated } from 'src/components/Authenticated';
3 | import ExtendedSidebarLayout from 'src/layouts/ExtendedSidebarLayout';
4 |
5 | import { useRedirectWithSession } from '@/constants/useRoute';
6 | import { PageLayout } from '@/types/resources';
7 |
8 | function Integrations() {
9 | useRedirectWithSession();
10 | return (
11 | <>
12 |
13 | Server Admin
14 |
15 | >
16 | );
17 | }
18 |
19 | Integrations.getLayout = (page: PageLayout) => (
20 |
21 | {page}
22 |
23 | );
24 |
25 | export default Integrations;
26 |
--------------------------------------------------------------------------------
/web-server/pages/system-logs.tsx:
--------------------------------------------------------------------------------
1 | import { Authenticated } from 'src/components/Authenticated';
2 |
3 | import { FlexBox } from '@/components/FlexBox';
4 | import { SystemStatus } from '@/components/Service/SystemStatus';
5 | import { useRedirectWithSession } from '@/constants/useRoute';
6 | import { PageWrapper } from '@/content/PullRequests/PageWrapper';
7 | import ExtendedSidebarLayout from '@/layouts/ExtendedSidebarLayout';
8 | import { useSelector } from '@/store';
9 | import { PageLayout } from '@/types/resources';
10 |
11 | function Service() {
12 | useRedirectWithSession();
13 |
14 | const loading = useSelector((state) => state.service.loading);
15 |
16 | return (
17 |
20 | System logs
21 |
22 | }
23 | hideAllSelectors
24 | pageTitle="System logs"
25 | showEvenIfNoTeamSelected={true}
26 | isLoading={loading}
27 | >
28 |
29 |
30 | );
31 | }
32 |
33 | Service.getLayout = (page: PageLayout) => (
34 |
35 | {page}
36 |
37 | );
38 |
39 | export default Service;
40 |
--------------------------------------------------------------------------------
/web-server/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from '@playwright/test';
2 |
3 | require('dotenv').config({ path: '.env.local' });
4 |
5 | const config: PlaywrightTestConfig = {
6 | reporter: [['html', { open: 'never' }]]
7 | };
8 |
9 | export default config;
10 |
--------------------------------------------------------------------------------
/web-server/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/web-server/public/assets/PAT_permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/assets/PAT_permissions.png
--------------------------------------------------------------------------------
/web-server/public/assets/gitlabPAT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/assets/gitlabPAT.png
--------------------------------------------------------------------------------
/web-server/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/favicon.ico
--------------------------------------------------------------------------------
/web-server/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/icon-192x192.png
--------------------------------------------------------------------------------
/web-server/public/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/icon-256x256.png
--------------------------------------------------------------------------------
/web-server/public/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/icon-384x384.png
--------------------------------------------------------------------------------
/web-server/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/icon-512x512.png
--------------------------------------------------------------------------------
/web-server/public/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/public/icon-96x96.png
--------------------------------------------------------------------------------
/web-server/public/imageStatusApiWorker.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('message', async (event) => {
2 | const { apiUrl, interval } = event.data;
3 | if (!apiUrl || !interval) {
4 | self.postMessage({ error: 'apiUrl and interval are required' });
5 | return;
6 | }
7 | const fetchData = async () => {
8 | try {
9 | const response = await fetch(apiUrl);
10 |
11 | const data = await response.json();
12 | self.postMessage({ data });
13 | } catch (error) {
14 | self.postMessage({ error: error.message });
15 | }
16 | };
17 |
18 | // Fetch data immediately
19 | await fetchData();
20 |
21 | // Set interval to fetch data periodically
22 | const intervalId = setInterval(fetchData, interval);
23 |
24 | // Listen for stop message to clear the interval
25 | self.addEventListener('message', (e) => {
26 | if (e.data === 'stop') {
27 | clearInterval(intervalId);
28 | }
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/web-server/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#6B1FB7",
3 | "background_color": "#FFFFFF",
4 | "display": "standalone",
5 | "start_url": ".",
6 | "short_name": "MiddlwareHQ",
7 | "name": "MiddlwareHQ",
8 | "icons": [
9 | {
10 | "src": "/icon-96x96.png",
11 | "sizes": "96x96",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "/icon-192x192.png",
16 | "sizes": "192x192",
17 | "type": "image/png"
18 | },
19 | {
20 | "src": "/icon-256x256.png",
21 | "sizes": "256x256",
22 | "type": "image/png"
23 | },
24 | {
25 | "src": "/icon-384x384.png",
26 | "sizes": "384x384",
27 | "type": "image/png"
28 | },
29 | {
30 | "src": "/icon-512x512.png",
31 | "sizes": "512x512",
32 | "type": "image/png"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/web-server/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow: /
4 |
--------------------------------------------------------------------------------
/web-server/public/static/images/placeholders/logo/google-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web-server/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -e
3 |
4 | DIR=$(dirname $0)
5 |
6 | source $DIR/utils.sh
7 | catch_force_exit
8 | is_project_root
9 |
10 | NEXT_MANUAL_SIG_HANDLE=true
11 | yarn run next build
12 |
13 | echo "EXITED $?"
14 |
15 | rm -rf .next/cache
16 | yarn run zip
17 |
--------------------------------------------------------------------------------
/web-server/scripts/server-init.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -e
3 |
4 | DIR=$(dirname $0)
5 |
6 | source $DIR/utils.sh
7 | is_project_root
8 |
9 | install_yarn_cmd_if_not_exists pm2
10 | # install_if_missing_vector
11 |
12 | set +e
13 | pm2 delete MHQ_HTTP_SERVER
14 | set -e
15 |
16 | NEXT_MANUAL_SIG_HANDLE=true
17 | pm2 start "yarn http" --name MHQ_HTTP_SERVER
18 |
--------------------------------------------------------------------------------
/web-server/scripts/utils.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -e
3 |
4 | function catch_force_exit() {
5 | # stty -echoctl
6 | trap force_exit INT
7 |
8 | force_exit() {
9 | echo "Force exiting..."
10 | }
11 | }
12 |
13 | function is_project_root() {
14 | if [ -f ./package.json ] && [ -f ./next.config.js ]; then
15 | return 0
16 | else
17 | echo "You must run this command from the project root.";
18 | exit 1
19 | fi
20 | }
21 |
22 | function install_yarn_cmd_if_not_exists() {
23 | if ! command -v $1; then
24 | yarn global add $1
25 | if ! command -v $1; then
26 | export PATH=$PATH:$(yarn global bin);
27 | if ! command -v $1; then echo "$1 command not found. exiting..."; fi
28 | fi
29 | else
30 | echo "$1 command exists"
31 | fi
32 | }
33 |
--------------------------------------------------------------------------------
/web-server/scripts/zip.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -e
3 |
4 | DIR=$(dirname $0)
5 |
6 | source $DIR/utils.sh
7 | is_project_root
8 |
9 | echo "Starting artifact creation"
10 |
11 | rm -rf artifacts
12 | mkdir artifacts
13 | tar -czf \
14 | artifacts/artifact.tar.gz \
15 | package.json \
16 | yarn.lock \
17 | http-server.js \
18 | .next \
19 | next.config.js \
20 | public \
21 | scripts \
22 |
23 |
24 | echo "Completed artifact creation"
25 |
--------------------------------------------------------------------------------
/web-server/src/api-helpers/axios-api-instance.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse } from 'axios';
2 | import axiosRetry from 'axios-retry';
3 |
4 | import { loggerInterceptor } from '@/api-helpers/axios';
5 |
6 | const _api = axios.create({
7 | baseURL: '/api'
8 | });
9 |
10 | const browserInterceptor = loggerInterceptor('browser');
11 | _api.interceptors.request.use(browserInterceptor);
12 |
13 | export const api = _api;
14 | axiosRetry(api, { retries: 2 });
15 | axiosRetry(axios, { retries: 2 });
16 |
17 | export const handleApiRaw = (
18 | url: string,
19 | params: AxiosRequestConfig = { method: 'get' }
20 | ): AxiosPromise =>
21 | api({
22 | url,
23 | ...params
24 | });
25 |
26 | export const handleApi = (
27 | url: string,
28 | params: AxiosRequestConfig = { method: 'get' }
29 | ): Promise =>
30 | handleApiRaw(url, {
31 | ...params,
32 | headers: { 'Content-Type': 'application/json' }
33 | })
34 | .then(handleThen)
35 | .catch(handleCatch);
36 |
37 | export const handleThen = (r: AxiosResponse) => r.data;
38 | export const handleCatch = (r: { response: AxiosResponse }) => {
39 | throw r.response;
40 | };
41 |
--------------------------------------------------------------------------------
/web-server/src/api-helpers/features.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from 'next/server';
2 | import { NextApiRequest } from 'next/types';
3 |
4 | import { PERSISTED_FLAG_KEY } from '@/constants/api';
5 | import { Features } from '@/constants/feature';
6 | import { ApiRequest } from '@/types/request';
7 |
8 | export const getFeaturesFromReq = (
9 | request: NextRequest | NextApiRequest
10 | ): Features => {
11 | const cookie =
12 | (request as NextRequest).cookies.get?.(PERSISTED_FLAG_KEY)?.value ||
13 | (request as NextApiRequest).cookies[PERSISTED_FLAG_KEY] ||
14 | '{}';
15 |
16 | return typeof cookie === 'string' ? JSON.parse(cookie) : cookie;
17 | };
18 |
19 | export const getFlagsFromRequest = (
20 | req: NextApiRequest
21 | ): ApiRequest['meta'] => {
22 | if (!req.query?.feature_flags) return undefined;
23 | return {
24 | features: JSON.parse(req.query.feature_flags as string)
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/web-server/src/assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/src/assets/background.png
--------------------------------------------------------------------------------
/web-server/src/assets/dora-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web-server/src/assets/fonts/Inter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/src/assets/fonts/Inter.ttf
--------------------------------------------------------------------------------
/web-server/src/assets/git-merge-line.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/web-server/src/components/AiButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps, useTheme } from '@mui/material';
2 | import { FC, useRef } from 'react';
3 | import { SiOpenai } from 'react-icons/si';
4 |
5 | import { track } from '@/constants/events';
6 |
7 | export const AiButton: FC void }> = ({
8 | onClickCallback,
9 | ...props
10 | }) => {
11 | const theme = useTheme();
12 | const mouseEnterRef = useRef(null);
13 |
14 | return (
15 |
30 | }
31 | onMouseEnter={() => {
32 | mouseEnterRef.current = setTimeout(
33 | () => track('AI_MODAL_BTN_LINGER'),
34 | 300
35 | );
36 | }}
37 | onMouseLeave={() => clearTimeout(mouseEnterRef.current)}
38 | >
39 | {props.children}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/web-server/src/components/AnimatedInputWrapper/slider.module.css:
--------------------------------------------------------------------------------
1 | .animationWrapper {
2 | position: absolute;
3 | top: 50%;
4 | transform: translate(0%, -50%);
5 | font-size: 1em;
6 | display: flex;
7 | gap: 4px;
8 | }
9 |
10 | .placeholder {
11 | height: 1.4em;
12 | width: 10rem;
13 | position: relative;
14 | }
15 |
16 | .repo {
17 | height: 1.4em;
18 | }
19 |
20 | .text {
21 | position: relative;
22 | transition: all 0.5s ease;
23 | }
24 |
25 | .textslide {
26 | display: flex;
27 | height: 1.4em;
28 | align-items: flex-start;
29 | }
30 |
--------------------------------------------------------------------------------
/web-server/src/components/AvatarPageTitle.tsx:
--------------------------------------------------------------------------------
1 | import { styled, Avatar, alpha } from '@mui/material';
2 |
3 | export const AvatarPageTitle = styled(Avatar)(({ theme }) => ({
4 | width: theme.spacing(4),
5 | height: theme.spacing(4),
6 | color: theme.colors.primary.main,
7 | marginTop: theme.spacing(-1),
8 | marginBottom: theme.spacing(-1),
9 | marginRight: theme.spacing(2),
10 | background: alpha(theme.colors.alpha.trueWhite[100], 0.05)
11 | }));
12 |
--------------------------------------------------------------------------------
/web-server/src/components/Chart2/index.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 |
3 | export const Chart2 = dynamic(() => import('./InternalChart2'), {
4 | ssr: false
5 | });
6 |
7 | export type {
8 | ChartLabels,
9 | ChartOnClick,
10 | ChartOnZoom,
11 | ChartOptions,
12 | ChartProps,
13 | ChartSeries,
14 | ChartType
15 | } from './InternalChart2';
16 |
17 | export { getChartZoomResetBtn, resetChartById } from './InternalChart2';
18 |
--------------------------------------------------------------------------------
/web-server/src/components/ErrorBoundaryFallback/err-pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/src/components/ErrorBoundaryFallback/err-pattern.png
--------------------------------------------------------------------------------
/web-server/src/components/FeatureFlagOverrides.tsx:
--------------------------------------------------------------------------------
1 | import { Slide } from '@mui/material';
2 | import { TransitionProps } from '@mui/material/transitions';
3 | import { forwardRef, ReactElement, Ref } from 'react';
4 |
5 | export const Transition = forwardRef(function Transition(
6 | props: TransitionProps & { children: ReactElement },
7 | ref: Ref
8 | ) {
9 | return ;
10 | });
11 |
--------------------------------------------------------------------------------
/web-server/src/components/HeaderBtn.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingButton, LoadingButtonProps } from '@mui/lab';
2 | import { useTheme } from '@mui/material';
3 | import { forwardRef } from 'react';
4 |
5 | export const HeaderBtn = forwardRef((props: LoadingButtonProps, ref: any) => {
6 | const theme = useTheme();
7 |
8 | return (
9 |
37 | );
38 | });
39 |
--------------------------------------------------------------------------------
/web-server/src/components/InsightChip.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowForwardRounded, InfoOutlined } from '@mui/icons-material';
2 | import { useTheme } from '@mui/material';
3 | import { FC, ReactNode } from 'react';
4 |
5 | import { FlexBox, FlexBoxProps } from '@/components/FlexBox';
6 | import { deepMerge } from '@/utils/datatype';
7 |
8 | export const InsightChip: FC<
9 | { startIcon?: ReactNode; endIcon?: ReactNode; cta?: ReactNode } & FlexBoxProps
10 | > = ({
11 | startIcon = startIconDefault,
12 | endIcon = endIconDefault,
13 | cta,
14 | children,
15 | ...props
16 | }) => {
17 | const theme = useTheme();
18 |
19 | return (
20 |
35 | {startIcon}
36 | {children}
37 | {cta}
38 | {endIcon}
39 |
40 | );
41 | };
42 |
43 | const startIconDefault = ;
44 | const endIconDefault = ;
45 |
--------------------------------------------------------------------------------
/web-server/src/components/LegendItem.tsx:
--------------------------------------------------------------------------------
1 | import { Box, useTheme } from '@mui/material';
2 | import { FC, ReactNode } from 'react';
3 |
4 | export const LegendItem: FC<{
5 | size?: 'default' | 'small';
6 | color: string;
7 | label: ReactNode;
8 | }> = ({ size: _size = 'default', color, label }) => {
9 | const theme = useTheme();
10 | const size = _size === 'default' ? 1.5 : 1;
11 | const fontSize = _size === 'default' ? '1em' : '0.8em';
12 | const gap = _size === 'default' ? 1 : 0.5;
13 | return (
14 |
15 |
21 |
22 | {label}
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/web-server/src/components/LegendsMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Box, useTheme } from '@mui/material';
2 | import { Serie } from '@nivo/line';
3 | import { FC } from 'react';
4 |
5 | import { FlexBox } from './FlexBox';
6 |
7 | export const LegendsMenu: FC<{
8 | series: Serie[];
9 | }> = ({ series }) => {
10 | const theme = useTheme();
11 |
12 | return (
13 |
21 | {series?.map((dataset, index) => {
22 | return (
23 |
24 |
25 |
26 | {dataset.id}
27 |
35 |
36 |
37 |
38 | );
39 | })}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/web-server/src/components/Loader/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, CircularProgress } from '@mui/material';
2 |
3 | import { getRandomLoadMsg } from '@/utils/loading-messages';
4 |
5 | import { Line } from '../Text';
6 |
7 | function Loader(props: { mode?: 'full-screen' | 'full-size' }) {
8 | const position = props.mode === 'full-size' ? 'absolute' : 'fixed';
9 |
10 | return (
11 |
19 |
20 | {getRandomLoadMsg()}
21 |
22 | Getting app data. Takes a moment...
23 |
24 |
25 | );
26 | }
27 |
28 | export default Loader;
29 |
--------------------------------------------------------------------------------
/web-server/src/components/Logo/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { FC, HTMLProps } from 'react';
2 |
3 | import LogoLongSvg from './logo-long.svg';
4 | import LogoSvg from './logo.svg';
5 |
6 | export const Logo: FC<
7 | HTMLProps & { mode?: 'short' | 'long' }
8 | > = (props) => {
9 | if (props.mode === 'long') return ;
10 | return ;
11 | };
12 |
--------------------------------------------------------------------------------
/web-server/src/components/Logo/logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/web-server/src/components/MiniButton.tsx:
--------------------------------------------------------------------------------
1 | import { SvgIconComponent } from '@mui/icons-material';
2 | import { LoadingButton, LoadingButtonProps } from '@mui/lab';
3 | import { FC, forwardRef } from 'react';
4 |
5 | export const MiniButton: FC<
6 | LoadingButtonProps & { Icon?: SvgIconComponent; place?: 'start' | 'end' }
7 | > = forwardRef(({ Icon, place = 'start', loading, ...props }, ref) => {
8 | return (
9 |
30 | )
31 | }
32 | endIcon={
33 | Icon &&
34 | place === 'end' && (
35 |
36 | )
37 | }
38 | >
39 | {props.children}
40 |
41 | );
42 | });
43 |
--------------------------------------------------------------------------------
/web-server/src/components/MiniLoader.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps, CircularProgress, LinearProgress } from '@mui/material';
2 | import { FC, ReactNode } from 'react';
3 |
4 | import { FlexBox, FlexBoxProps } from './FlexBox';
5 |
6 | export const MiniLoader: FC<{ label: ReactNode } & BoxProps> = ({
7 | label,
8 | ...props
9 | }) => (
10 |
11 | {label}
12 |
13 |
14 | );
15 |
16 | export const MiniCircularLoader: FC<
17 | {
18 | label: ReactNode;
19 | position?: 'start' | 'end';
20 | } & FlexBoxProps
21 | > = ({ label, position = 'start', ...props }) => (
22 |
23 | {position === 'end' && label}
24 |
25 | {position === 'start' && label}
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/web-server/src/components/MotionComponents.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mui/material';
2 | import { motion } from 'framer-motion';
3 |
4 | export const MotionBox = motion(Box);
5 |
--------------------------------------------------------------------------------
/web-server/src/components/NoTeamSelected.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Typography } from '@mui/material';
2 | import Link from 'next/link';
3 | import { FC } from 'react';
4 |
5 | import NoTeamSelectedSvg from '@/assets/no-team-selected.svg';
6 | import { ROUTES } from '@/constants/routes';
7 |
8 | export const NoTeamSelected: FC = () => {
9 | return (
10 |
17 |
18 |
19 | Select a team to get started
20 |
21 |
22 | Want to create a new one?
23 |
24 |
25 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/web-server/src/components/OverlayComponents/ChangeFailureRate.tsx:
--------------------------------------------------------------------------------
1 | import { FlexBox } from '../FlexBox';
2 |
3 | export const ChangeFailureRate = () => {
4 | return ChangeFailureRate;
5 | };
6 |
--------------------------------------------------------------------------------
/web-server/src/components/OverlayComponents/Dummy.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | import { FlexBox } from '../FlexBox';
4 | import { Line } from '../Text';
5 |
6 | export const Dummy = () => {
7 | const router = useRouter();
8 | return (
9 |
10 |
11 | Hi! This is a dummy component!
12 |
13 |
14 | {JSON.stringify(router.query, null, ' ')}
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/web-server/src/components/OverlayComponents/TeamEdit.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import { CRUDProps } from '@/components/Teams/CreateTeams';
4 | import { usePageRefreshCallback } from '@/hooks/usePageRefreshCallback';
5 |
6 | import { FlexBox } from '../FlexBox';
7 | import { useOverlayPage } from '../OverlayPageContext';
8 | import { CreateEditTeams } from '../Teams/CreateTeams';
9 |
10 | export const TeamEdit: FC = ({ teamId }) => {
11 | const { removeAll } = useOverlayPage();
12 | const pageRefreshCallback = usePageRefreshCallback();
13 |
14 | return (
15 |
16 | {
20 | removeAll();
21 | pageRefreshCallback();
22 | }}
23 | />
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/web-server/src/components/PageContentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { ErrorBoundary } from 'react-error-boundary';
3 |
4 | import { ErrorBoundaryFallback } from '@/components/ErrorBoundaryFallback/index';
5 |
6 | import { FlexBox, FlexBoxProps } from './FlexBox';
7 | import Scrollbar from './Scrollbar';
8 |
9 | export const PageContentWrapper: FC<
10 | FlexBoxProps & { noScrollbars?: boolean }
11 | > = ({ children, noScrollbars, ...props }) => {
12 | const content = (
13 |
24 |
25 | {children}
26 |
27 |
28 | );
29 |
30 | if (noScrollbars) return content;
31 |
32 | return {content};
33 | };
34 |
--------------------------------------------------------------------------------
/web-server/src/components/PageTitleWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mui/material';
2 | import { BoxProps } from '@mui/system';
3 | import PropTypes from 'prop-types';
4 | import { FC } from 'react';
5 |
6 | const DefaultPageTitleWrapper: FC = ({ children, ...props }) => {
7 | return (
8 |
14 | {children}
15 |
16 | );
17 | };
18 |
19 | DefaultPageTitleWrapper.propTypes = {
20 | children: PropTypes.node.isRequired
21 | };
22 |
23 | export default DefaultPageTitleWrapper;
24 |
25 | export const PageTitleWrapper: FC = ({ children, ...props }) => {
26 | return (
27 |
34 | {children}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/web-server/src/components/RepoCard.tsx:
--------------------------------------------------------------------------------
1 | import { DataObjectRounded } from '@mui/icons-material';
2 | import { Typography, styled } from '@mui/material';
3 |
4 | import GitBranch from '@/assets/git-merge-line.svg';
5 | import { Repo } from '@/types/github';
6 | import { DB_OrgRepo } from '@/types/resources';
7 |
8 | export const RepoTitle = styled(Typography)(() => ({
9 | textOverflow: 'ellipsis',
10 | whiteSpace: 'nowrap',
11 | overflow: 'hidden',
12 | display: 'block',
13 | cursor: 'pointer'
14 | }));
15 |
16 | export const RepoDescription = styled(Typography)(() => ({
17 | width: '100%',
18 | textOverflow: 'ellipsis',
19 | overflow: 'hidden',
20 | display: 'block'
21 | }));
22 |
23 | export const RepoLangIcon = styled(DataObjectRounded)(({ theme }) => ({
24 | marginLeft: theme.spacing(-1 / 4),
25 | marginRight: theme.spacing(1 / 2),
26 | opacity: 0.8,
27 | height: '0.7em',
28 | width: '0.7em'
29 | }));
30 |
31 | export const GitBranchIcon = styled(GitBranch)(({ theme }) => ({
32 | marginRight: theme.spacing(0.5),
33 | opacity: 0.8,
34 | height: '1.5em',
35 | width: '1.5em'
36 | }));
37 |
38 | export const adaptDbRepo = (repo: DB_OrgRepo): Partial => ({
39 | name: repo.name,
40 | html_url: `//${repo.provider}.com/${repo.org_name}/${repo.name}`,
41 | language: repo.language,
42 | default_branch: repo.default_branch
43 | });
44 |
--------------------------------------------------------------------------------
/web-server/src/components/Service/SystemLog/PlainLog.tsx:
--------------------------------------------------------------------------------
1 | import { Line } from '@/components/Text';
2 |
3 | export const PlainLog = ({ log }: { log: string; index: number }) => {
4 | return (
5 |
6 | {log}
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/web-server/src/components/TeamSelector/defaultPopoverProps.tsx:
--------------------------------------------------------------------------------
1 | import { PopoverProps } from '@mui/material';
2 |
3 | export const defaultPopoverProps: Partial = {
4 | disableScrollLock: true,
5 | anchorOrigin: {
6 | vertical: 'top',
7 | horizontal: 'left'
8 | },
9 | transformOrigin: {
10 | vertical: 'top',
11 | horizontal: 'left'
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/web-server/src/components/TicketsTableAddons/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import { SearchRounded } from '@mui/icons-material';
2 | import ClearRoundedIcon from '@mui/icons-material/ClearRounded';
3 | import { IconButton, InputAdornment, TextField } from '@mui/material';
4 | import { FC } from 'react';
5 |
6 | export const SearchInput: FC<{
7 | inputHandler: (inputText: string) => void;
8 | inputText: string;
9 | }> = ({ inputHandler, inputText }) => {
10 | return (
11 | inputHandler(e.target.value)}
16 | InputProps={{
17 | startAdornment: ,
18 | endAdornment: (
19 |
20 | {inputText && (
21 | inputHandler('')}
24 | edge="end"
25 | >
26 |
27 |
28 | )}
29 |
30 | )
31 | }}
32 | sx={{ width: '350px' }}
33 | />
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/web-server/src/components/TopLevelLogicComponent.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from 'react';
2 |
3 | import { useImageUpdateStatusWorker } from '@/hooks/useImageUpdateStatusWorker';
4 | import { getGithubRepoStars } from '@/slices/app';
5 | import { useDispatch } from '@/store';
6 |
7 | export const TopLevelLogicComponent: FC = () => {
8 | const dispatch = useDispatch();
9 |
10 | useEffect(() => {
11 | dispatch(getGithubRepoStars());
12 | }, [dispatch, getGithubRepoStars]);
13 |
14 | useImageUpdateStatusWorker();
15 |
16 | return null;
17 | };
18 |
--------------------------------------------------------------------------------
/web-server/src/constants/api.ts:
--------------------------------------------------------------------------------
1 | import { Integration } from './integrations';
2 |
3 | export const PERSISTED_FLAG_KEY = `application-persisted-feature-flags`;
4 |
5 | export const CODE_PROVIDER_INTEGRATIONS_MAP = {
6 | [Integration.GITHUB]: Integration.GITHUB,
7 | [Integration.GITLAB]: Integration.GITLAB,
8 | [Integration.BITBUCKET]: Integration.BITBUCKET
9 | };
10 |
--------------------------------------------------------------------------------
/web-server/src/constants/feature.ts:
--------------------------------------------------------------------------------
1 | export const defaultFlags = {
2 | dummy_function_flag: (_args: { orgId: ID }) => true,
3 | use_mock_data: false,
4 | enable_pr_cycle_time_comparison: false,
5 | use_hotkeys: true,
6 | show_deployment_settings: false,
7 | show_incident_settings: false
8 | };
9 |
10 | export type Features = typeof defaultFlags;
11 |
--------------------------------------------------------------------------------
/web-server/src/constants/generic.ts:
--------------------------------------------------------------------------------
1 | export const MAX_INT = 2147483647 - 1;
2 | export const SAFE_DELIMITER = '╡';
3 | export const ASCII_SAFE_DELIM = '/`/,/`/';
4 | export const ONE_KB = 1024 * 1024;
5 | export const ONE_MB = ONE_KB * 1024;
6 | export const END_OF_TIME = new Date(1e15);
7 |
--------------------------------------------------------------------------------
/web-server/src/constants/integrations.ts:
--------------------------------------------------------------------------------
1 | export enum Integration {
2 | GOOGLE = 'google',
3 | JIRA = 'jira',
4 | SLACK = 'slack',
5 | GITHUB = 'github',
6 | BITBUCKET = 'bitbucket',
7 | GITLAB = 'gitlab',
8 | ZENDUTY = 'zenduty',
9 | PAGERDUTY = 'pagerduty',
10 | OPSGENIE = 'opsgenie',
11 | MICROSOFT = 'azure-ad',
12 | CIRCLECI = 'circle_ci'
13 | }
14 |
15 | export enum CIProvider {
16 | GITHUB_ACTIONS = 'GITHUB_ACTIONS',
17 | CIRCLE_CI = 'CIRCLE_CI'
18 | }
19 |
20 | export enum WorkflowType {
21 | DEPLOYMENT = 'DEPLOYMENT'
22 | }
23 |
--------------------------------------------------------------------------------
/web-server/src/constants/log-formatter.ts:
--------------------------------------------------------------------------------
1 | export const generalLogRegex =
2 | /^\[(.*?)\] \[(\d+)\] \[(INFO|ERROR|WARN|DEBUG|WARNING|CRITICAL)\] (.+)$/;
3 | export const httpLogRegex =
4 | /^(\S+) (\S+) (\S+) \[([^\]]+)\] "([^"]*)" (\d+) (\d+) "([^"]*)" "([^"]*)"$/;
5 | export const redisLogRegex =
6 | /^(?\d+:[XCMS]) (?\d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2}\.\d{3}) (?[\.\-\#\*]) (?.*)$/;
7 | export const postgresLogRegex =
8 | /^(?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC) \[\d+\] (?[A-Z]+):\s*(?(.|\n)+?)$/;
9 | export const dataSyncLogRegex = /\[(\w+)\]\s(.+?)\sfor\s(\w+)\s(.+)/;
10 |
--------------------------------------------------------------------------------
/web-server/src/constants/notification.ts:
--------------------------------------------------------------------------------
1 | export const DURATION = 2000;
2 | export const SUCCESS = 'success';
3 | export const ERROR = 'error';
4 | export const DIRECT_ADDED = 'Direct Added Successfully!!';
5 | export const MANAGER_ADDED = 'Manager Added successfully!!';
6 | export const ERROR_ADDING_USER = 'Error Adding User';
7 | export const UNKNOWN_ERROR = 'Unknown Error Occured';
8 | export const USER_DELETED = 'User Deleted Succesfully';
9 |
--------------------------------------------------------------------------------
/web-server/src/constants/relations.ts:
--------------------------------------------------------------------------------
1 | export enum UserRelations {
2 | MANAGER = 'MANAGER',
3 | DIRECT = 'DIRECT'
4 | }
5 |
--------------------------------------------------------------------------------
/web-server/src/constants/service.ts:
--------------------------------------------------------------------------------
1 | export enum ServiceNames {
2 | API_SERVER = 'api-server-service',
3 | REDIS = 'redis-service',
4 | POSTGRES = 'postgres-service',
5 | SYNC_SERVER = 'sync-server-service'
6 | }
7 |
--------------------------------------------------------------------------------
/web-server/src/constants/stream.ts:
--------------------------------------------------------------------------------
1 | import { ServiceNames } from './service';
2 |
3 | type LogFile = {
4 | path: string;
5 | serviceName: ServiceNames;
6 | };
7 |
8 | type ServiceStatus = Record;
9 |
10 | type LogUpdateData = {
11 | serviceName: ServiceNames;
12 | content: string;
13 | };
14 |
15 | type StatusUpdateData = {
16 | statuses: ServiceStatus;
17 | };
18 |
19 | type SendEventData = LogUpdateData | StatusUpdateData;
20 |
21 | const UPDATE_INTERVAL = 10000;
22 |
23 | const LOG_FILES: LogFile[] = [
24 | {
25 | path: '/var/log/apiserver/apiserver.log',
26 | serviceName: ServiceNames.API_SERVER
27 | },
28 | {
29 | path: '/var/log/sync_server/sync_server.log',
30 | serviceName: ServiceNames.SYNC_SERVER
31 | },
32 | {
33 | path: '/var/log/redis/redis.log',
34 | serviceName: ServiceNames.REDIS
35 | },
36 | {
37 | path: '/var/log/postgres/postgres.log',
38 | serviceName: ServiceNames.POSTGRES
39 | }
40 | ];
41 |
42 | enum StreamEventType {
43 | StatusUpdate = 'status-update',
44 | LogUpdate = 'log-update'
45 | }
46 |
47 | enum FileEvent {
48 | Change = 'change'
49 | }
50 |
51 | export type { LogFile, ServiceStatus, SendEventData };
52 | export { UPDATE_INTERVAL, LOG_FILES, StreamEventType, FileEvent };
53 |
--------------------------------------------------------------------------------
/web-server/src/constants/ui-states.ts:
--------------------------------------------------------------------------------
1 | export enum FetchState {
2 | REQUEST = 'REQUEST',
3 | SUCCESS = 'SUCCESS',
4 | FAILURE = 'FAILURE',
5 | DORMANT = 'DORMANT',
6 | RETRIAL = 'RETRIAL'
7 | }
8 |
--------------------------------------------------------------------------------
/web-server/src/constants/urls.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_GH_URL = 'https://api.github.com';
2 |
--------------------------------------------------------------------------------
/web-server/src/content/Cockpit/codeMetrics/shared.tsx:
--------------------------------------------------------------------------------
1 | import { DateValueTuple } from '@/types/resources';
2 |
3 | export const getTrendsDataFromArray = (trendsArr: DateValueTuple[]) => {
4 | return trendsArr?.map((t) => t[1]).flat() || [];
5 | };
6 |
--------------------------------------------------------------------------------
/web-server/src/content/Dashboards/githubIntegration.tsx:
--------------------------------------------------------------------------------
1 | import faker from '@faker-js/faker';
2 | import { GitHub } from '@mui/icons-material';
3 |
4 | import GitlabIcon from '@/mocks/icons/gitlab.svg';
5 |
6 | export const githubIntegrationsDisplay = {
7 | id: faker.datatype.uuid(),
8 | type: 'github',
9 | name: 'Github',
10 | description: 'Code insights & blockers',
11 | color: '#fff',
12 | bg: `linear-gradient(135deg, hsla(160, 10%, 61%, 0.6) 0%, hsla(247, 0%, 21%, 0.6) 100%)`,
13 | icon:
14 | };
15 |
16 | export const gitLabIntegrationDisplay = {
17 | id: '39936e43-178a-4272-bef3-948d770bc98f',
18 | type: 'gitlab',
19 | name: 'Gitlab',
20 | description: 'Code insights & blockers',
21 | color: '#554488',
22 | bg: 'linear-gradient(-45deg, hsla(17, 95%, 50%, 0.6) 0%, hsla(42, 94%, 67%, 0.6) 100%)',
23 | icon:
24 | } as IntegrationItem;
25 |
26 | export type IntegrationItem = typeof githubIntegrationsDisplay;
27 |
--------------------------------------------------------------------------------
/web-server/src/content/Dashboards/useIntegrationHandlers.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import { Integration } from '@/constants/integrations';
4 | import { ConfigureGitlabModalBody } from '@/content/Dashboards/ConfigureGitlabModalBody';
5 | import { useModal } from '@/contexts/ModalContext';
6 | import { useAuth } from '@/hooks/useAuth';
7 | import { unlinkProvider } from '@/utils/auth';
8 |
9 | import { ConfigureGithubModalBody } from './ConfigureGithubModalBody';
10 |
11 | export const useIntegrationHandlers = () => {
12 | const { orgId } = useAuth();
13 |
14 | const { addModal, closeAllModals } = useModal();
15 |
16 | return useMemo(() => {
17 | const handlers = {
18 | link: {
19 | github: () =>
20 | addModal({
21 | title: 'Configure Github',
22 | body: ,
23 | showCloseIcon: true
24 | }),
25 | gitlab: () =>
26 | addModal({
27 | title: 'Configure Gitlab',
28 | body: ,
29 | showCloseIcon: true
30 | })
31 | },
32 | unlink: {
33 | github: () => unlinkProvider(orgId, Integration.GITHUB),
34 | gitlab: () => unlinkProvider(orgId, Integration.GITLAB)
35 | }
36 | };
37 |
38 | return handlers;
39 | }, [addModal, closeAllModals, orgId]);
40 | };
41 |
--------------------------------------------------------------------------------
/web-server/src/content/DoraMetrics/DoraCards/NoIncidentsLabel.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import { Line } from '@/components/Text';
4 |
5 | export const NoIncidentsLabel: FC<{ deploymentsCount?: number }> = ({
6 | deploymentsCount
7 | }) => (deploymentsCount ? No incidents : No CFR data);
8 |
--------------------------------------------------------------------------------
/web-server/src/content/DoraMetrics/DoraCards/sharedComponents.tsx:
--------------------------------------------------------------------------------
1 | import { Paper, useTheme } from '@mui/material';
2 | import Img from 'next/image';
3 |
4 | import { FlexBox, FlexBoxProps } from '@/components/FlexBox';
5 |
6 | export const CardRoot = (props: FlexBoxProps) => (
7 |
24 | );
25 |
26 | export const NoDataImg = () => {
27 | const theme = useTheme();
28 | return (
29 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/web-server/src/content/DoraMetrics/MetricsClassificationsThreshold.ts:
--------------------------------------------------------------------------------
1 | import {
2 | secondsInDay,
3 | secondsInHour,
4 | secondsInMonth,
5 | secondsInWeek
6 | } from 'date-fns/constants';
7 |
8 | export const changeTimeThresholds = {
9 | elite: secondsInDay,
10 | high: secondsInWeek,
11 | medium: secondsInMonth
12 | };
13 |
14 | export const deploymentFrequencyThresholds = {
15 | elite: 7,
16 | high: 1,
17 | medium: 1 / 30
18 | };
19 |
20 | export const updatedDeploymentFrequencyThresholds = (metric: {
21 | count: number;
22 | interval: 'day' | 'week' | 'month';
23 | }): 'elite' | 'high' | 'medium' | 'low' => {
24 | switch (metric.interval) {
25 | case 'day':
26 | if (metric.count >= 1) return 'elite';
27 | break;
28 | case 'week':
29 | if (metric.count >= 1) return 'high';
30 | break;
31 | case 'month':
32 | if (metric.count === 1) return 'medium';
33 | else if (metric.count > 1) return 'high';
34 | break;
35 | default:
36 | return 'low';
37 | }
38 | return 'low';
39 | };
40 |
41 | export const changeFailureRateThresholds = {
42 | elite: 5,
43 | high: 10,
44 | medium: 15
45 | };
46 |
47 | export const meanTimeToRestoreThresholds = {
48 | elite: secondsInHour,
49 | high: secondsInDay,
50 | medium: secondsInWeek
51 | };
52 |
--------------------------------------------------------------------------------
/web-server/src/content/DoraMetrics/getDoraLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { GoLinkExternal } from 'react-icons/go';
3 |
4 | import { Line } from '@/components/Text';
5 | import { OPEN_IN_NEW_TAB_PROPS } from '@/utils/url';
6 |
7 | export const getDoraLink = (text: string) => (
8 |
13 |
27 | {text}
28 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/web-server/src/content/PullRequests/DeploymentFrequencyGraph.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { GoLinkExternal } from 'react-icons/go';
3 |
4 | import { Line } from '@/components/Text';
5 | import { OPEN_IN_NEW_TAB_PROPS } from '@/utils/url';
6 |
7 | export const getDoraLink = (text: string) => (
8 |
13 |
27 | {text}
28 |
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/web-server/src/contexts/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": ["add-react-displayname"]
4 | }
5 |
--------------------------------------------------------------------------------
/web-server/src/contexts/SidebarContext.tsx:
--------------------------------------------------------------------------------
1 | import { useState, ReactNode, createContext } from 'react';
2 | type SidebarContext = {
3 | sidebarToggle: any;
4 | toggleSidebar: () => void;
5 | closeSidebar: () => void;
6 | };
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-redeclare
9 | export const SidebarContext = createContext(
10 | {} as SidebarContext
11 | );
12 |
13 | type Props = {
14 | children: ReactNode;
15 | };
16 |
17 | export function SidebarProvider({ children }: Props) {
18 | const [sidebarToggle, setSidebarToggle] = useState(false);
19 | const toggleSidebar = () => {
20 | setSidebarToggle(!sidebarToggle);
21 | };
22 |
23 | const closeSidebar = () => {
24 | setSidebarToggle(false);
25 | };
26 |
27 | return (
28 |
31 | {children}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/web-server/src/createEmotionCache.ts:
--------------------------------------------------------------------------------
1 | import createCache from '@emotion/cache';
2 | // import stylisRTLPlugin from 'stylis-plugin-rtl';
3 |
4 | export default function createEmotionCache() {
5 | return createCache({
6 | key: 'css'
7 | // // @ts-ignore
8 | // stylisPlugins: [stylisRTLPlugin]
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useActiveRouteEvent.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useMemo } from 'react';
3 |
4 | import { TrackEvents } from '@/constants/events';
5 | import { ROUTES } from '@/constants/routes';
6 |
7 | type AllowedEventTypes = 'APP_TEAM_CHANGE_SINGLE' | 'APP_DATE_RANGE_CHANGED';
8 |
9 | // Please sync event names in events.ts too while updating below map
10 | const FEATURE_EVENT_PREFIX_MAP = {
11 | [ROUTES.DORA_METRICS.PATH]: 'DORA_METRICS'
12 | };
13 |
14 | export const useActiveRouteEvent = (
15 | appEventType: AllowedEventTypes
16 | ): keyof typeof TrackEvents => {
17 | const router = useRouter();
18 | const activePath = router.pathname;
19 | return useMemo(
20 | () =>
21 | (FEATURE_EVENT_PREFIX_MAP[activePath]
22 | ? FEATURE_EVENT_PREFIX_MAP[activePath] + appEventType.split('APP')[1]
23 | : appEventType) as keyof typeof TrackEvents,
24 | [activePath, appEventType]
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { AuthContext } from 'src/contexts/ThirdPartyAuthContext';
3 |
4 | export const useAuth = () => useContext(AuthContext);
5 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useCountUp.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const STEPS = 7; // number of steps to reach the target value
4 | const INTERVAL = 75; // in ms
5 |
6 | export const useCountUp = (
7 | targetValue: number,
8 | decimalPlaces: number = 0
9 | ): number => {
10 | const [count, setCount] = useState(0);
11 |
12 | useEffect(() => {
13 | let currentStep = 0;
14 | const stepValue = targetValue / STEPS;
15 |
16 | const timer = setInterval(() => {
17 | currentStep++;
18 |
19 | if (currentStep >= STEPS) {
20 | setCount(parseFloat(targetValue.toFixed(decimalPlaces)));
21 | clearInterval(timer);
22 | } else {
23 | const newValue = stepValue * currentStep;
24 | setCount(parseFloat(newValue.toFixed(decimalPlaces)));
25 | }
26 | }, INTERVAL);
27 |
28 | return () => clearInterval(timer);
29 | }, [targetValue, decimalPlaces]);
30 |
31 | return count;
32 | };
33 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useDoraMetricsGraph/utils.ts:
--------------------------------------------------------------------------------
1 | import { secondsInDay, secondsInHour } from 'date-fns/constants';
2 |
3 | import { indexify } from '@/utils/datatype';
4 |
5 | export const calculateMaxScale = (
6 | graphData: { id: string; value: number }[]
7 | ) => {
8 | const maxVal = Math.max(...graphData.map((s) => s.value));
9 | return Math.ceil(maxVal / secondsInDay) * secondsInDay;
10 | };
11 |
12 | export const calculateTicks = (maxScale: number) => {
13 | const days = Math.round(maxScale / secondsInDay);
14 | const hours = Math.round(maxScale / secondsInHour);
15 | const inDays = days > 2;
16 | const inWeeks = days > 7;
17 |
18 | if (inWeeks) {
19 | const WEEK_FACTOR = calculateWeekFactor(days);
20 | const ticks = Array.from(
21 | { length: Math.round(days / WEEK_FACTOR) + 1 },
22 | indexify
23 | );
24 | return ticks.map((tick) => tick * secondsInDay * WEEK_FACTOR);
25 | } else if (inDays) {
26 | const ticks = Array.from({ length: days + 1 }, indexify);
27 | return ticks.map((tick) => tick * secondsInDay);
28 | }
29 |
30 | // Sub 2 days
31 | const HOUR_FACTOR = hours >= 24 ? 6 : 3;
32 | const ticks = Array.from(
33 | { length: Math.round(hours / HOUR_FACTOR) + 1 },
34 | indexify
35 | );
36 | return ticks.map((tick) => tick * secondsInHour * HOUR_FACTOR);
37 | };
38 |
39 | export const calculateWeekFactor = (days: number) => {
40 | if (days < 12) return 2;
41 | if (days >= 12 && days < 21) return 3;
42 | return 7;
43 | };
44 |
--------------------------------------------------------------------------------
/web-server/src/hooks/usePageRefreshCallback.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | import { ROUTES } from '@/constants/routes';
4 | import { useAuth } from '@/hooks/useAuth';
5 | import {
6 | useBranchesForPrFilters,
7 | useSingleTeamConfig
8 | } from '@/hooks/useStateTeamConfig';
9 | import { fetchTeamDoraMetrics } from '@/slices/dora_metrics';
10 | import { useDispatch } from '@/store';
11 |
12 | export const usePageRefreshCallback = () => {
13 | const router = useRouter();
14 | const dispatch = useDispatch();
15 | const { orgId } = useAuth();
16 | const { dates, singleTeamId } = useSingleTeamConfig();
17 | const branchPayloadForPrFilters = useBranchesForPrFilters();
18 |
19 | switch (router.pathname) {
20 | case ROUTES.DORA_METRICS.PATH:
21 | return () =>
22 | dispatch(
23 | fetchTeamDoraMetrics({
24 | orgId,
25 | teamId: singleTeamId,
26 | fromDate: dates.start,
27 | toDate: dates.end,
28 | ...branchPayloadForPrFilters
29 | })
30 | );
31 | default:
32 | return () => {};
33 | }
34 | // TODO: Pending routes to implement
35 | // ROUTES.PROJECT_MANAGEMENT.PATH
36 | // ROUTES.COLLABORATE.METRICS.PATH
37 | // ROUTES.COLLABORATE.METRICS.USER.PATH
38 | // ROUTES.COLLABORATE.METRICS.CODEBASE.PATH
39 | };
40 |
--------------------------------------------------------------------------------
/web-server/src/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, MutableRefObject } from 'react';
2 |
3 | export const usePrevious = (value: T): T => {
4 | const ref: MutableRefObject = useRef();
5 | useEffect(() => {
6 | ref.current = value;
7 | }, [value]);
8 | return ref.current;
9 | };
10 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useRefMounted.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react';
2 |
3 | export const useRefMounted = () => {
4 | const isRef = useRef(false);
5 |
6 | useEffect(() => {
7 | isRef.current = true;
8 |
9 | return () => {
10 | isRef.current = false;
11 | };
12 | }, []);
13 |
14 | return useCallback(() => isRef.current, []);
15 | };
16 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useResizeEventTracking.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | import { track } from '@/constants/events';
4 |
5 | export const useResizeEventTracking = () => {
6 | const timerRef = useRef>(null);
7 |
8 | useEffect(() => {
9 | if (typeof window === 'undefined') return;
10 |
11 | const listener = () => {
12 | clearTimeout(timerRef.current);
13 | timerRef.current = setTimeout(() => track('WINDOW_RESIZE'), 1000);
14 | };
15 |
16 | window.addEventListener('resize', listener);
17 |
18 | return () => {
19 | window.removeEventListener('resize', listener);
20 | };
21 | }, []);
22 | };
23 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useScrollTop.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useEffect } from 'react';
3 |
4 | const useScrollTop = (): null => {
5 | const location = useRouter();
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0);
9 | }, [location.pathname]);
10 |
11 | return null;
12 | };
13 |
14 | export default useScrollTop;
15 |
--------------------------------------------------------------------------------
/web-server/src/hooks/useSystemLogs.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import { ServiceNames } from '@/constants/service';
4 | import { useSelector } from '@/store';
5 | export const useSystemLogs = ({
6 | serviceName
7 | }: {
8 | serviceName?: ServiceNames;
9 | }) => {
10 | const services = useSelector((state) => state.service.services);
11 | const loading = useSelector((state) => state.service.loading);
12 | const logs = useMemo(
13 | () => services[serviceName]?.logs || [],
14 | [serviceName, services]
15 | );
16 |
17 | return {
18 | services,
19 | loading,
20 | logs
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/web-server/src/layouts/ExtendedSidebarLayout/Sidebar/SidebarMenu/MenuWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { Box, styled } from '@mui/material';
2 |
3 | export const MenuWrapper = styled(Box)(
4 | ({ theme }) => `
5 | .MuiList-root {
6 | padding: ${theme.spacing(1)};
7 |
8 | & > .MuiList-root {
9 | padding: 0 ${theme.spacing(0)} ${theme.spacing(1)};
10 | }
11 | }
12 |
13 | .MuiListSubheader-root {
14 | text-transform: uppercase;
15 | font-weight: bold;
16 | font-size: ${theme.typography.pxToRem(12)};
17 | color: ${theme.colors.alpha.trueWhite[50]};
18 | padding: ${theme.spacing(0, 2.5)};
19 | line-height: 1.4;
20 | }
21 | `
22 | );
23 |
--------------------------------------------------------------------------------
/web-server/src/layouts/ExtendedSidebarLayout/Sidebar/SidebarTopSection/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mui/material';
2 |
3 | import { Logo } from '@/components/Logo/Logo';
4 |
5 | function SidebarTopSection() {
6 | return (
7 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default SidebarTopSection;
23 |
--------------------------------------------------------------------------------
/web-server/src/mocks/icons/circleci-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web-server/src/mocks/icons/jira-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web-server/src/mocks/icons/zenduty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/middlewarehq/middleware/e0a05821509101745fab8eb361e69cad84ab934a/web-server/src/mocks/icons/zenduty.png
--------------------------------------------------------------------------------
/web-server/src/mocks/repos.ts:
--------------------------------------------------------------------------------
1 | export const teamReposMock = [
2 | {
3 | org_id: '23d9e173-e98d-4ffd-b025-b5e7dbf0962f',
4 | name: 'web-manager-dash',
5 | provider: 'github',
6 | created_at: '2022-04-15T16:26:40.855853+00:00',
7 | updated_at: '2022-05-02T16:06:39.386+00:00',
8 | id: '328d4e5d-ae5d-45f9-9818-66f56110a3a9',
9 | org_name: 'monoclehq',
10 | is_active: true
11 | },
12 | {
13 | org_id: '23d9e173-e98d-4ffd-b025-b5e7dbf0962f',
14 | name: 'monorepo',
15 | provider: 'github',
16 | created_at: '2022-04-15T16:26:40.855853+00:00',
17 | updated_at: '2022-05-02T16:06:39.386+00:00',
18 | id: '5b79d8e1-7133-48dc-876d-0670495800c2',
19 | org_name: 'monoclehq',
20 | is_active: true
21 | }
22 | ];
23 |
24 | export const incidentSourceMock = {
25 | created_at: '2024-04-05T07:37:06.720174+00:00',
26 | updated_at: '2024-04-05T07:37:06.720231+00:00',
27 | org_id: 'd9b3d829-9b51-457f-85ab-107d20119524',
28 | setting: {
29 | incident_sources: ['INCIDENT_SERVICE']
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/web-server/src/slices/actions.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | interface State {
4 | sortOrder: 'asc' | 'desc';
5 | showActionsWithoutTeams: boolean;
6 | }
7 |
8 | const initialState: State = {
9 | sortOrder: 'asc',
10 | showActionsWithoutTeams: false
11 | };
12 |
13 | export const actionsSlice = createSlice({
14 | name: 'actions',
15 | initialState,
16 | reducers: {
17 | toggleOrder(state: State): void {
18 | state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc';
19 | },
20 | toggleActionsWithoutTeams(state: State): void {
21 | state.showActionsWithoutTeams = !state.showActionsWithoutTeams;
22 | }
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/web-server/src/store/rootReducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from '@reduxjs/toolkit';
2 |
3 | import { actionsSlice } from '@/slices/actions';
4 | import { appSlice } from '@/slices/app';
5 | import { authSlice } from '@/slices/auth';
6 | import { doraMetricsSlice } from '@/slices/dora_metrics';
7 | import { loadLinkSlice } from '@/slices/loadLink';
8 | import { orgSlice } from '@/slices/org';
9 | import { serviceSlice } from '@/slices/service';
10 | import { teamSlice } from '@/slices/team';
11 |
12 | export const rootReducer = combineReducers({
13 | app: appSlice.reducer,
14 | auth: authSlice.reducer,
15 | actions: actionsSlice.reducer,
16 | team: teamSlice.reducer,
17 | org: orgSlice.reducer,
18 | doraMetrics: doraMetricsSlice.reducer,
19 | loadLink: loadLinkSlice.reducer,
20 | service: serviceSlice.reducer
21 | });
22 |
--------------------------------------------------------------------------------
/web-server/src/theme/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from '@mui/material';
2 | import { StylesProvider } from '@mui/styles';
3 | import { FC, useState, createContext, useEffect } from 'react';
4 |
5 | import { themeCreator } from './base';
6 |
7 | export const ThemeContext = createContext((_themeName: string): void => {});
8 |
9 | const ThemeProviderWrapper: FC = (props) => {
10 | const [themeName, _setThemeName] = useState('NebulaFighterTheme');
11 |
12 | useEffect(() => {
13 | const curThemeName =
14 | window.localStorage.getItem('appTheme') || 'NebulaFighterTheme';
15 | _setThemeName(curThemeName);
16 | }, []);
17 |
18 | const theme = themeCreator(themeName);
19 | const setThemeName = (themeName: string): void => {
20 | window.localStorage.setItem('appTheme', themeName);
21 | _setThemeName(themeName);
22 | };
23 |
24 | return (
25 |
26 |
27 | {props.children}
28 |
29 |
30 | );
31 | };
32 |
33 | export default ThemeProviderWrapper;
34 |
--------------------------------------------------------------------------------
/web-server/src/types/api/teams.ts:
--------------------------------------------------------------------------------
1 | export interface Team {
2 | id: string;
3 | org_id: string;
4 | name: string;
5 | member_ids: string[];
6 | manager_id?: string;
7 | created_at: Date;
8 | updated_at: Date;
9 | is_deleted: boolean;
10 | member_filter_enabled?: boolean;
11 | }
12 |
13 | export type BaseTeam = {
14 | id: string;
15 | name: string;
16 | member_ids: string[];
17 | org_id?: string;
18 | };
19 |
--------------------------------------------------------------------------------
/web-server/src/types/github.ts:
--------------------------------------------------------------------------------
1 | import { GhType, Github } from './octokit';
2 |
3 | export type GhRepo = GhType[number];
4 | export type Repo = GhRepo;
5 |
6 | export type LoadedOrg = {
7 | name?: string;
8 | avatar_url: string;
9 | login: string;
10 | repos: string[];
11 | web_url: string;
12 | };
13 |
--------------------------------------------------------------------------------
/web-server/src/types/octokit.ts:
--------------------------------------------------------------------------------
1 | import { Octokit } from '@octokit/rest';
2 |
3 | export const Github = new Octokit();
4 | export type { GetResponseDataTypeFromEndpointMethod as GhType } from '@octokit/types';
5 |
--------------------------------------------------------------------------------
/web-server/src/types/redux.ts:
--------------------------------------------------------------------------------
1 | import { FetchState } from '@/constants/ui-states';
2 |
3 | export type StateRequests = Partial<
4 | Record, FetchState>
5 | >;
6 |
7 | export type StateErrors = Partial, any>>;
8 |
9 | export type StateFetchConfig = S & {
10 | requests?: StateRequests;
11 | errors?: StateErrors;
12 | };
13 |
--------------------------------------------------------------------------------
/web-server/src/types/request.ts:
--------------------------------------------------------------------------------
1 | import { Features } from '@/constants/feature';
2 |
3 | import type { NextApiRequest, NextApiResponse } from 'next/types';
4 |
5 | export type HttpMethods =
6 | | 'GET'
7 | | 'POST'
8 | | 'PUT'
9 | | 'PATCH'
10 | | 'DELETE'
11 | | 'OPTIONS'
12 | | 'HEAD';
13 |
14 | export type ApiRequest = Omit<
15 | NextApiRequest,
16 | 'body' | 'query' | 'method'
17 | > & {
18 | /** @deprecated Use `req.payload` instead */
19 | body: T;
20 | /** @deprecated Use `req.payload` instead */
21 | query: T;
22 | payload: T;
23 | meta?: {
24 | features: Partial;
25 | };
26 | method: HttpMethods;
27 | };
28 |
29 | export type ApiResponse = NextApiResponse;
30 |
--------------------------------------------------------------------------------
/web-server/src/utils/__tests__/domainCheck.test.ts:
--------------------------------------------------------------------------------
1 | import { checkDomainWithRegex } from '../domainCheck';
2 |
3 | describe('checkDomainWithRegex', () => {
4 | const validDomains = [
5 | 'http://example.com',
6 | 'https://example.com',
7 | 'https://sub.example.co.uk',
8 | 'http://example.io:8080',
9 | 'https://example.io:8080',
10 | 'https://example.com/',
11 | 'https://123domain.net',
12 | 'http://my-domain.org'
13 | ];
14 |
15 | test.each(validDomains)('returns true for %s', (domain) => {
16 | expect(checkDomainWithRegex(domain)).toBe(true);
17 | });
18 |
19 | const invalidDomains = [
20 | 'example.com',
21 | 'ftp://example.com',
22 | 'http:/example.com',
23 | 'https//example.com',
24 | 'https://-example.com',
25 | 'https://example-.com',
26 | 'https://example',
27 | 'https://.com',
28 | 'https://example:toolongtsadasds',
29 | 'https://example.com:999999',
30 | 'https://example .com',
31 | 'https://example.com/ path',
32 | '',
33 | 'https://',
34 | 'https:///'
35 | ];
36 |
37 | test.each(invalidDomains)('returns false for %s', (domain) => {
38 | expect(checkDomainWithRegex(domain)).toBe(false);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/web-server/src/utils/adapt_deployments.ts:
--------------------------------------------------------------------------------
1 | import { descend, mapObjIndexed, prop, sort } from 'ramda';
2 |
3 | import {
4 | Deployment,
5 | UpdatedDeployment,
6 | UpdatedTeamDeploymentsApiResponse
7 | } from '@/types/resources';
8 |
9 | export function adaptDeploymentsMap(curr: UpdatedDeployment): Deployment {
10 | return {
11 | id: curr.id,
12 | status: curr.status,
13 | head_branch: curr.head_branch,
14 | event_actor: {
15 | username: curr.event_actor.username,
16 | linked_user: curr.event_actor.linked_user
17 | },
18 | created_at: '',
19 | updated_at: '',
20 | conducted_at: curr.conducted_at,
21 | pr_count: curr.pr_count,
22 | html_url: curr.html_url,
23 | repo_workflow_id: curr.meta.repo_workflow_id,
24 | run_duration: curr.duration
25 | };
26 | }
27 |
28 | export const adaptedDeploymentsMap = (
29 | deploymentsMap: UpdatedTeamDeploymentsApiResponse['deployments_map']
30 | ) => {
31 | const x = Object.entries(deploymentsMap).map(([key, value]) => {
32 | return [key, value.map(adaptDeploymentsMap)];
33 | });
34 | const adaptedDeployments: Record =
35 | Object.fromEntries(x);
36 |
37 | return mapObjIndexed(sort(descend(prop('conducted_at'))), adaptedDeployments);
38 | };
39 |
--------------------------------------------------------------------------------
/web-server/src/utils/auth-supplementary.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { splitEvery } from 'ramda';
3 |
4 | import { privateDecrypt, publicEncrypt } from 'crypto';
5 |
6 | const CHUNK_SIZE = 127;
7 | export const enc = (data?: string) => {
8 | const key = Buffer.from(process.env.SECRET_PUBLIC_KEY, 'base64');
9 | try {
10 | return data
11 | ? splitEvery(CHUNK_SIZE, data).map((chunk) =>
12 | publicEncrypt(key, Buffer.from(chunk)).toString('base64')
13 | )
14 | : null;
15 | } catch (e) {
16 | return null;
17 | }
18 | };
19 |
20 | export const dec = (chunks: string[]) => {
21 | const key = Buffer.from(process.env.SECRET_PRIVATE_KEY, 'base64');
22 | return chunks
23 | .map((chunk) => privateDecrypt(key, Buffer.from(chunk, 'base64')))
24 | .join('');
25 | };
26 |
27 | export const INTEGRATION_CONFLICT_COLUMNS = ['org_id', 'name'];
28 |
29 | export const validateGithubToken = async (token: string) => {
30 | try {
31 | const response = await axios.get('https://api.github.com/user/repos', {
32 | headers: {
33 | // @ts-ignore
34 | Authorization: `token ${dec(token)}`
35 | }
36 | });
37 | return response.status === 200;
38 | } catch (error: any) {
39 | console.error('Token validation error:', error.response);
40 | return false;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/web-server/src/utils/code.ts:
--------------------------------------------------------------------------------
1 | import { PR } from '@/types/resources';
2 |
3 | export const getKeyForPr = (pr: PR) => `${pr.number}/${pr.repo_name}`;
4 |
5 | export const getCycleTimeForPr = (pr: PR) =>
6 | pr.first_response_time + pr.rework_time + pr.merge_time || 0;
7 |
--------------------------------------------------------------------------------
/web-server/src/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | export const debounce = any>(
2 | fn: T,
3 | duration: number
4 | ): any => {
5 | let timerId: NodeJS.Timeout | null = null;
6 |
7 | return (...args: any[]) => {
8 | timerId && clearTimeout(timerId);
9 | timerId = setTimeout(() => fn(...args), duration);
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/web-server/src/utils/domainCheck.ts:
--------------------------------------------------------------------------------
1 | export const checkDomainWithRegex = (domain: string) => {
2 | const regex =
3 | /^(https?:\/\/)[A-Za-z0-9]+([-.][A-Za-z0-9]+)*\.[A-Za-z]{2,}(:[0-9]{1,5})?(\/\S*)?$/;
4 | return regex.test(domain);
5 | };
6 |
--------------------------------------------------------------------------------
/web-server/src/utils/enum.ts:
--------------------------------------------------------------------------------
1 | export const objectEnum = (EnumArg: EnumT) => {
2 | type EnumKeys = keyof typeof EnumArg;
3 |
4 | return Object.keys(EnumArg).reduce(
5 | // @ts-ignore
6 | (obj, key) => ({ ...obj, [key]: key }),
7 | {} as { [Property in EnumKeys]: Property }
8 | );
9 | };
10 |
11 | export const objectEnumFromFn = (enumFn: () => EnumT) =>
12 | objectEnum(enumFn());
13 |
--------------------------------------------------------------------------------
/web-server/src/utils/fn.ts:
--------------------------------------------------------------------------------
1 | export const depFn = (fn: T, ...args: Parameters) =>
2 | fn?.(...args);
3 |
4 | export const noOp = () => {};
5 |
--------------------------------------------------------------------------------
/web-server/src/utils/loading-messages.ts:
--------------------------------------------------------------------------------
1 | const LOADING_MESSAGES = [
2 | 'Hoping your numbers look great? Me too! 🤞',
3 | "Hang on. I'll grab lunch and get fresh data for you... any minute now...",
4 | 'And the oscar goes to... YOU! For hanging on while we get these stats for you',
5 | 'Loading... like watching paint dry, but with data 🎨',
6 | 'Hang on, one of our employees ran away with your data 🏃♂️ (jk 👀)',
7 | 'Grabbing lunch! Erm... data. I mean data.',
8 | 'How badly do you actually want that cycle time/sprint spillover?',
9 | 'Your team is doing juuuust fiiiine... trust me.',
10 | 'How many times did you ask your team for updates today?',
11 | 'Samad is bringing your insights on a skateboard 🛹',
12 | 'Shivam is literally typing out the API response right now. 1 sec ⏳',
13 | 'Eshaan stayed up all night to do the math for this 🌙',
14 | "Look out of your window! It's Amogh with your data! 📨",
15 | "Adnan doesn't think your stats are half bad. He thinks they are half good! 👌"
16 | ];
17 |
18 | function getRandomSeededInt(max: number): number {
19 | const currentTime = new Date().getTime();
20 | const timeBasedSeed = Math.floor(currentTime / 5000); // 5 seconds window
21 | const seed = timeBasedSeed % (max + 1);
22 | return seed;
23 | }
24 |
25 | export const getRandomLoadMsg = () =>
26 | LOADING_MESSAGES[getRandomSeededInt(LOADING_MESSAGES.length - 1)];
27 |
--------------------------------------------------------------------------------
/web-server/src/utils/mock.ts:
--------------------------------------------------------------------------------
1 | import faker from '@faker-js/faker';
2 |
3 | export const staticArray = (
4 | length: number,
5 | proc = (num: number) => num
6 | ) =>
7 | Array(Math.max(Number.isFinite(length) ? length : 0, 0))
8 | .fill(0)
9 | .map((_, i) => proc(i)) as unknown as T[];
10 |
11 | export const arraySize = (max: number = 5, min: number = 0) =>
12 | staticArray(randInt(max, min));
13 |
14 | export const flexibleArray = arraySize;
15 |
16 | export const randomDuration = () =>
17 | faker.datatype.number({ min: 0, max: 10, precision: 0.1 });
18 |
19 | export const randInt = (n1: number, n2: number = 0) =>
20 | faker.datatype.number({ min: Math.min(n1, n2), max: Math.max(n1, n2) });
21 |
22 | export const arrayDivByN = (arr: Array, parts: number = 1) => {
23 | const chunkSize = Math.floor(arr.length / parts);
24 | const rem = arr.length % parts;
25 | let start = 0;
26 | let end = chunkSize + rem;
27 | const res = [arr.slice(start, end)];
28 | for (let i = 1; i < parts; i++) {
29 | start = end;
30 | end = end + chunkSize;
31 | res.push(arr.slice(start, end));
32 | }
33 | return res;
34 | };
35 |
--------------------------------------------------------------------------------
/web-server/src/utils/objectArray.ts:
--------------------------------------------------------------------------------
1 | import { path } from 'ramda';
2 |
3 | export const groupBy = []>(
4 | arr: T,
5 | key: keyof T[number] = 'id',
6 | keyPath?: string
7 | ): Record =>
8 | arr.reduce((acc, cur) => {
9 | acc[path(((keyPath || key) as string).split('.'), cur)] = cur;
10 | return acc;
11 | }, {});
12 |
13 | export const groupObj = groupBy;
14 |
15 | export default groupBy;
16 |
--------------------------------------------------------------------------------
/web-server/src/utils/randomId.ts:
--------------------------------------------------------------------------------
1 | export const randomId = (): string => {
2 | const arr = new Uint8Array(12);
3 | window.crypto.getRandomValues(arr);
4 | return Array.from(arr, (v) => v.toString(16).padStart(2, '0')).join('');
5 | };
6 |
--------------------------------------------------------------------------------
/web-server/src/utils/redux.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AsyncThunk,
3 | ActionReducerMapBuilder,
4 | Draft,
5 | PayloadAction
6 | } from '@reduxjs/toolkit';
7 |
8 | import { FetchState } from '@/constants/ui-states';
9 | import { StateFetchConfig } from '@/types/redux';
10 |
11 | export const addFetchCasesToReducer = <
12 | S extends StateFetchConfig<{}>,
13 | T extends AsyncThunk
14 | >(
15 | builder: ActionReducerMapBuilder,
16 | thunk: T,
17 | key: keyof S['requests'],
18 | onSuccess?: (
19 | state: Draft,
20 | action: PayloadAction['payload']>
21 | ) => any,
22 | onFailure?: (state: Draft, action: PayloadAction) => any
23 | ) => {
24 | builder.addCase(thunk.fulfilled, (state, action) => {
25 | if (!state.requests) state.requests = {};
26 |
27 | onSuccess?.(state, action);
28 | // @ts-ignore
29 | state.requests[key] = FetchState.SUCCESS;
30 | });
31 |
32 | builder.addCase(thunk.pending, (state) => {
33 | if (!state.requests) state.requests = {};
34 | // @ts-ignore
35 | state.requests[key] = FetchState.REQUEST;
36 | });
37 |
38 | builder.addCase(thunk.rejected, (state, action) => {
39 | if (!state.requests) state.requests = {};
40 | if (!state.errors) state.errors = {};
41 | // @ts-ignore
42 | state.requests[key as string] = FetchState.FAILURE;
43 | // @ts-ignore
44 | state.errors[key as string] = action.error as string;
45 | onFailure?.(state, action);
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/web-server/src/utils/storage.ts:
--------------------------------------------------------------------------------
1 | import Cookies, { CookieAttributes } from 'js-cookie';
2 |
3 | const attrs: CookieAttributes = {
4 | expires: 30, // days
5 | path: '/',
6 | secure: true
7 | };
8 |
9 | export const storage = {
10 | get: (key: string) => {
11 | if (typeof window === 'undefined') return;
12 | const value = Cookies.get(key);
13 | try {
14 | return value && JSON.parse(value);
15 | } catch (e) {
16 | return value;
17 | }
18 | },
19 | set: (key: string, value: any) => {
20 | if (typeof window === 'undefined') return;
21 | if (typeof value === 'string') {
22 | Cookies.set(key, value, attrs);
23 | } else {
24 | Cookies.set(key, JSON.stringify(value), attrs);
25 | }
26 | },
27 | remove: (key: string) => {
28 | if (typeof window === 'undefined') return;
29 | Cookies.remove(key, attrs);
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/web-server/src/utils/stringFormatting.ts:
--------------------------------------------------------------------------------
1 | import { DatumValue } from '@nivo/core';
2 | import pluralize from 'pluralize';
3 | export const trimWithEllipsis = (
4 | text: string,
5 | maxTextLength: number,
6 | addInStart?: boolean
7 | ) => {
8 | const diff = text.length - maxTextLength;
9 | if (diff <= 3) return text;
10 | const textStr = addInStart
11 | ? `...${text.slice(text.length - maxTextLength)}`
12 | : `${text.slice(0, maxTextLength)}...`;
13 | return textStr;
14 | };
15 |
16 | export const pluralizePrCount = (value: number) =>
17 | `${value === 1 ? 'PR' : 'PRs'}`;
18 |
19 | export const formatAsPercent = (value: DatumValue) =>
20 | value ? `${value}%` : `0%`;
21 |
22 | export const formatAsDeployment = (value: number) =>
23 | value >= 1000
24 | ? `${value / 1000}k Deps`
25 | : `${value} ${pluralize('deps', value)}`;
26 |
27 | export const joinNames = (names: string[]): string => {
28 | if (names.length === 0) {
29 | return '';
30 | } else if (names.length === 1) {
31 | return names[0];
32 | } else {
33 | const lastNames = names.slice(-1);
34 | const otherNames = names.slice(0, -1);
35 | return `${otherNames.join(', ')} and ${lastNames[0]}`;
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/web-server/src/utils/unistring.ts:
--------------------------------------------------------------------------------
1 | import FastestValidator from 'fastest-validator';
2 |
3 | // unid = unistring ID
4 |
5 | const uuidValidator = new FastestValidator().compile({
6 | uuid: { type: 'uuid' }
7 | });
8 |
9 | export const isUuid = (id: ID) => uuidValidator({ uuid: id });
10 |
11 | /**
12 | * #### Unistring ID utils
13 | * ---
14 | * UNID Examples
15 | *
16 | * Team: `TEAM_` /
17 | * User: `USER_`
18 | */
19 | export const unid = {
20 | /** Get unistring ID for a user, from a valid UUID */
21 | u: (id: string) => isUuid(id) && `USER_${id}`,
22 | /** Get unistring ID for a team, from a valid UUID */
23 | t: (id: string) => isUuid(id) && `TEAM_${id}`,
24 | /** Get UUID from a unistring ID */
25 | id: (un_id: string) => un_id.split('_')[1]
26 | };
27 |
28 | const isValidUnid = (unid: ID, type: string) => {
29 | const [typePart, idPart] = unid.split('_')[0];
30 | return type === typePart && isUuid(idPart);
31 | };
32 |
33 | export const isUnid = {
34 | u: (id: string) => isValidUnid(id, 'USER'),
35 | t: (id: string) => isValidUnid(id, 'TEAM')
36 | };
37 |
--------------------------------------------------------------------------------
/web-server/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | export const getUrlParam = (param: string) => {
2 | if (typeof window === 'undefined') return null;
3 | return new URLSearchParams(window.location.search).get(param);
4 | };
5 |
6 | export const OPEN_IN_NEW_TAB_PROPS = {
7 | target: '_blank',
8 | rel: 'noopener nofollow noreferrer'
9 | };
10 |
--------------------------------------------------------------------------------
/web-server/src/utils/user.ts:
--------------------------------------------------------------------------------
1 | import { Row } from '@/constants/db';
2 | import { brandColors } from '@/theme/schemes/theme';
3 | import { BaseUser } from '@/types/resources';
4 |
5 | export type UserProfile = User;
6 |
7 | export const getAvatar = (user?: User): string | undefined => {
8 | if (user?.identities?.github?.username)
9 | return getGHAvatar(user?.identities?.github?.username);
10 | else if (user?.identities?.bitbucket?.meta?.avatar_url)
11 | return user?.identities?.bitbucket?.meta?.avatar_url;
12 | };
13 |
14 | export const getAvatarObj = (url: string) => ({
15 | avatar_url: { href: url }
16 | });
17 |
18 | export const getGHAvatar = (handle?: string, size: number = 128) =>
19 | handle && `https://github.com/${handle}.png?size=${size}`;
20 |
21 | export const getLangIcon = (lang: string) =>
22 | `https://cdn.jsdelivr.net/gh/devicons/devicon/icons/${lang}/${lang}-plain.svg`;
23 |
24 | export const getBaseUserFromRowUser = (user: Row<'Users'>): BaseUser => ({
25 | email: user.primary_email,
26 | id: user.id,
27 | name: user.name,
28 | avatar_url: null
29 | });
30 |
31 | export const getColorByStatus = (status: 'MERGED' | 'CLOSED' | 'OPEN') => {
32 | switch (status) {
33 | case 'OPEN':
34 | return brandColors.pr.open;
35 | case 'CLOSED':
36 | return brandColors.pr.close;
37 | case 'MERGED':
38 | default:
39 | return brandColors.pr.merge;
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/web-server/src/utils/wait.ts:
--------------------------------------------------------------------------------
1 | export const wait = (milliseconds: number): Promise =>
2 | new Promise((res) => setTimeout(res, milliseconds));
3 |
--------------------------------------------------------------------------------
/web-server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"],
6 | "@/public/*": ["./public/*"],
7 | "@/api/*": ["./pages/api/*"]
8 | },
9 | "allowJs": true,
10 | "allowSyntheticDefaultImports": true,
11 | "jsx": "preserve",
12 | "lib": ["dom", "es2017", "ES2021", "es2021.intl", "ES2022"],
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "noEmit": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "preserveConstEnums": true,
19 | "removeComments": false,
20 | "skipLibCheck": true,
21 | "sourceMap": true,
22 | "strict": true,
23 | "strictPropertyInitialization": false,
24 | "strictNullChecks": false,
25 | "target": "esnext",
26 | "forceConsistentCasingInFileNames": true,
27 | "esModuleInterop": true,
28 | "resolveJsonModule": true,
29 | "isolatedModules": true,
30 | "noFallthroughCasesInSwitch": true,
31 | "incremental": true,
32 | "plugins": [
33 | {
34 | "name": "next"
35 | }
36 | ]
37 | },
38 | "exclude": ["node_modules"],
39 | "include": [
40 | "src",
41 | "next-env.d.ts",
42 | "**/*.ts",
43 | "**/*.tsx",
44 | ".next/types/**/*.ts"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------