From 60ca028ae01c2e22f842d103a50c84dfb527ddbb Mon Sep 17 00:00:00 2001 From: Maksim Sviridov Date: Mon, 9 Sep 2024 13:02:25 +0300 Subject: [PATCH] fix(ExternalTasks): don`t destroy service on startup --- src/components/CriteriaForm/CriteriaForm.tsx | 19 ++-- .../GoalActivityFeed/GoalActivityFeed.tsx | 3 +- src/components/GoalCriteriaSuggest.tsx | 3 + src/components/Page/Page.tsx | 7 +- src/utils/declareSsrProps.ts | 14 ++- src/utils/integration/jira.ts | 89 +++++++++++-------- src/utils/pageContext.ts | 3 + trpc/router/jira.ts | 5 +- 8 files changed, 93 insertions(+), 50 deletions(-) diff --git a/src/components/CriteriaForm/CriteriaForm.tsx b/src/components/CriteriaForm/CriteriaForm.tsx index 74ea59458..260f18a79 100644 --- a/src/components/CriteriaForm/CriteriaForm.tsx +++ b/src/components/CriteriaForm/CriteriaForm.tsx @@ -172,6 +172,7 @@ interface CriteriaFormProps { withModeSwitch?: boolean; values?: CriteriaFormValues; validityData: ValidityData; + externalAllowed?: boolean; setMode: (mode: CriteriaFormMode) => void; onSubmit: (values: CriteriaFormValues) => void; @@ -308,6 +309,7 @@ export const CriteriaForm = ({ onReset, items, withModeSwitch, + externalAllowed, validityData, validateBindingsFor, values, @@ -333,11 +335,18 @@ export const CriteriaForm = ({ const isEditMode = values != null && !!values.title?.length; - const radios: Array<{ value: CriteriaFormMode; title: string }> = [ - { title: tr('Simple'), value: 'simple' }, - { title: tr('Goal'), value: 'goal' }, - { title: tr('Task'), value: 'task' }, - ]; + const radios = useMemo>(() => { + const base: Array<{ value: CriteriaFormMode; title: string }> = [ + { title: tr('Simple'), value: 'simple' }, + { title: tr('Goal'), value: 'goal' }, + ]; + + if (externalAllowed) { + base.push({ title: tr('Task'), value: 'task' }); + } + + return base; + }, [externalAllowed]); const title = watch('title'); const selected = watch('selected'); diff --git a/src/components/GoalActivityFeed/GoalActivityFeed.tsx b/src/components/GoalActivityFeed/GoalActivityFeed.tsx index 3d95da6a4..2ccb5ab15 100644 --- a/src/components/GoalActivityFeed/GoalActivityFeed.tsx +++ b/src/components/GoalActivityFeed/GoalActivityFeed.tsx @@ -37,7 +37,7 @@ type AddCriteriaMode = NonNullable( ({ goal, shortId, onGoalDeleteConfirm, onInvalidate }, ref) => { - const { user } = usePageContext(); + const { user, allowedServices } = usePageContext(); const { onGoalCommentUpdate, onGoalDelete, @@ -217,6 +217,7 @@ export const GoalActivityFeed = forwardRef id.id)]} + externalAllowed={allowedServices?.jira || false} /> ))} diff --git a/src/components/GoalCriteriaSuggest.tsx b/src/components/GoalCriteriaSuggest.tsx index 3d643bd96..0c8a6b086 100644 --- a/src/components/GoalCriteriaSuggest.tsx +++ b/src/components/GoalCriteriaSuggest.tsx @@ -34,6 +34,7 @@ interface GoalCriteriaSuggestProps { versa?: boolean; /** Value allows restrict search results by current user */ restrictedSearch?: boolean; + externalAllowed?: boolean; withModeSwitch?: React.ComponentProps['withModeSwitch']; defaultMode?: React.ComponentProps['mode']; values?: React.ComponentProps['values']; @@ -57,6 +58,7 @@ export const GoalCriteriaSuggest: React.FC = ({ validityData, onGoalSelect, restrictedSearch = false, + externalAllowed = false, }) => { const [mode, setMode] = useState(defaultMode); const [query, setQuery] = useState(''); @@ -174,6 +176,7 @@ export const GoalCriteriaSuggest: React.FC = ({ items={itemsToRender} validityData={validityData} validateBindingsFor={validateBindings} + externalAllowed={externalAllowed} /> ); }; diff --git a/src/components/Page/Page.tsx b/src/components/Page/Page.tsx index f84336576..216ee7683 100644 --- a/src/components/Page/Page.tsx +++ b/src/components/Page/Page.tsx @@ -57,6 +57,9 @@ export const Page: React.FC = ({ user, ssrTime, title = 'Untitled', c const { setPreview } = useGoalPreview(); const { data: userSettings = user?.settings } = trpc.user.settings.useQuery(); const { data: config } = trpc.appConfig.get.useQuery(); + const { data: jiraIsEnable = false } = trpc.jira.isEnable.useQuery(undefined, { + staleTime: Infinity, + }); const router = useRouter(); @@ -80,7 +83,9 @@ export const Page: React.FC = ({ user, ssrTime, title = 'Untitled', c }, [router, userSettings]); return ( - + {title} diff --git a/src/utils/declareSsrProps.ts b/src/utils/declareSsrProps.ts index 000a7bfb6..3907fb98c 100644 --- a/src/utils/declareSsrProps.ts +++ b/src/utils/declareSsrProps.ts @@ -10,6 +10,8 @@ import type { TrpcRouter } from '../../trpc/router'; import { transformer } from './transformer'; import { setSSRLocale, TLocale } from './getLang'; +type IntegrationServices = 'jira'; + export interface SSRProps

{ user: Session['user']; req: GetServerSidePropsContext['req']; @@ -17,6 +19,9 @@ export interface SSRProps

{ query: Record; ssrTime: number; ssrHelpers: DecoratedProcedureSSGRecord; + allowedServices?: { + [key in IntegrationServices]: boolean; + }; } export interface ExternalPageProps

extends SSRProps

{ @@ -52,9 +57,12 @@ export function declareSsrProps( transformer, }); - await ssrHelpers.appConfig.get.fetch(); - await ssrHelpers.v2.project.userProjects.fetch({}); - await ssrHelpers.filter.getUserFilters.fetch(); + await Promise.all([ + ssrHelpers.appConfig.get.fetch(), + ssrHelpers.v2.project.userProjects.fetch({}), + ssrHelpers.filter.getUserFilters.fetch(), + ssrHelpers.jira.isEnable.fetch(), + ]); const ssrTime = Date.now(); diff --git a/src/utils/integration/jira.ts b/src/utils/integration/jira.ts index 9fab7bde8..73f078d08 100644 --- a/src/utils/integration/jira.ts +++ b/src/utils/integration/jira.ts @@ -83,31 +83,46 @@ const isDebugEnabled = process.env.NODE_ENV === 'development' && process.env.DEB // @ts-ignore class JiraService extends JiraApi { - private positiveFinishedStatuses: string; - - private finishedStatusCategory: JiraIssueStatus['statusCategory']; - - constructor(options: JiraApi.JiraApiOptions) { - super(options); - - assert( - process.env.JIRA_POSITIVE_STATUS_NAMES, - `Env variable \`JIRA_POSITIVE_STATUS_NAMES\` must be string, but get ${typeof process.env - .JIRA_POSITIVE_STATUS_NAMES}`, - ); - assert( - process.env.JIRA_FINISHED_CATEGORY, - `Env variable \`JIRA_FINISHED_CATEGORY\` must be string, but get ${typeof process.env - .JIRA_FINISHED_CATEGORY}`, - ); - - this.positiveFinishedStatuses = process.env.JIRA_POSITIVE_STATUS_NAMES; - this.finishedStatusCategory = JSON.parse(process.env.JIRA_FINISHED_CATEGORY); - - assert( - typeof this.finishedStatusCategory === 'object' && this.finishedStatusCategory != null, - "Env variable 'JIRA_FINISHED_CATEGORY' must be JSON string value", - ); + private positiveFinishedStatuses = ''; + + private finishedStatusCategory?: JiraIssueStatus['statusCategory']; + + public isEnable = false; + + constructor() { + super({ + protocol: 'https', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + host: process.env.JIRA_URL!, + username: process.env.JIRA_USER, + password: process.env.JIRA_PASSWORD, + apiVersion: process.env.JIRA_API_VERSION, + strictSSL: process.env.NODE_ENV === 'production', + }); + + try { + // check env variables which contains `JIRA` prefix + Object.keys(process.env) + .filter((k) => k.includes('JIRA')) + .forEach((jiraEnvKey) => { + const val = process.env[jiraEnvKey]; + + assert(val, `Env variable \`${jiraEnvKey}\` must be string, but get ${typeof val}`); + }); + + // here suppoed that env variable is available + this.positiveFinishedStatuses = process.env.JIRA_POSITIVE_STATUS_NAMES as string; + this.finishedStatusCategory = JSON.parse(process.env.JIRA_FINISHED_CATEGORY as string); + + assert( + typeof this.finishedStatusCategory === 'object' && this.finishedStatusCategory != null, + "Env variable 'JIRA_FINISHED_CATEGORY' must be JSON string value", + ); + this.isEnable = true; + } catch (error) { + console.error(error); + this.isEnable = false; + } } /** start overriding private instance methods */ @@ -137,23 +152,19 @@ class JiraService extends JiraApi { /** end overriding private instance methods */ public checkStatusIsFinished(status: JiraIssueStatus) { - return ( - (status.statusCategory.key === this.finishedStatusCategory.key || - status.statusCategory.id === this.finishedStatusCategory.id) && - this.positiveFinishedStatuses.includes(status.name) - ); + if (this.isEnable && this.finishedStatusCategory) { + return ( + (status.statusCategory.key === this.finishedStatusCategory.key || + status.statusCategory.id === this.finishedStatusCategory.id) && + this.positiveFinishedStatuses.includes(status.name) + ); + } + + return false; } } -export const jiraService = new JiraService({ - protocol: 'https', - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - host: process.env.JIRA_URL!, - username: process.env.JIRA_USER, - password: process.env.JIRA_PASSWORD, - apiVersion: process.env.JIRA_API_VERSION, - strictSSL: process.env.NODE_ENV === 'production', -}); +export const jiraService = new JiraService(); const re = '(\\w+)-(\\d+)'; diff --git a/src/utils/pageContext.ts b/src/utils/pageContext.ts index 9bae14168..71eb1b34c 100644 --- a/src/utils/pageContext.ts +++ b/src/utils/pageContext.ts @@ -6,6 +6,9 @@ export interface PageContext { themeId: number; ssrTime: number; user?: Session['user']; + allowedServices?: { + jira: boolean; + }; } export const pageContext = React.createContext({ theme: 'dark', themeId: 0, ssrTime: 0 }); diff --git a/trpc/router/jira.ts b/trpc/router/jira.ts index 89472e9a8..ee41ba4ab 100644 --- a/trpc/router/jira.ts +++ b/trpc/router/jira.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { protectedProcedure, router } from '../trpcBackend'; -import { JiraIssue, searchIssue } from '../../src/utils/integration/jira'; +import { JiraIssue, searchIssue, jiraService } from '../../src/utils/integration/jira'; import { ExternalTask } from '../../generated/kysely/types'; import { ExtractTypeFromGenerated } from '../utils'; @@ -30,6 +30,9 @@ const jiraIssueToExternalTask = ( }; export const jira = router({ + isEnable: protectedProcedure.query(async () => { + return Promise.resolve(jiraService.isEnable); + }), search: protectedProcedure .input( z.object({