diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..56a595fd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,294 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Setup +```bash +yarn # Install dependencies (requires Node.js >=20.x) +``` + +### Development +```bash +yarn start # Start dev server on localhost:3000 +yarn build # Production build to ./build directory +yarn lint # Run ESLint +yarn lint:styles # Run stylelint on styled-components +``` + +### Notes +- This project uses **Yarn 4.5.1** as the package manager +- Development server runs on port 3000 (configured in vite.config.ts) +- Build output is in `./build` directory (not `dist`) +- No test suite is currently configured + +## High-Level Architecture + +### Tech Stack +- **React 18** with TypeScript (strict mode) +- **Vite** for build tooling +- **Redux Toolkit** for state management +- **React Router v6** for routing +- **Ant Design v5** + styled-components for UI +- **Axios** for HTTP with interceptors + +### Application Purpose +Penify (Snorkell) Dashboard is a SaaS platform for automated code documentation generation and analysis. It integrates with multiple Git platforms (GitHub, GitLab, Bitbucket, Azure DevOps) to provide: +- Function-level documentation coverage analysis +- AI-powered documentation generation +- Code quality metrics (complexity, maintainability) +- README quality analysis +- Security vulnerability scanning + +### Core Architectural Patterns + +#### 1. Multi-Vendor Git Platform Integration +The app is vendor-agnostic and supports 4 Git platforms through a unified abstraction: + +```typescript +// Routes follow pattern: /repositories/{vendor}/{orgName}/{repoName} +// vendor = GITHUB | GITLAB | BITBUCKET | AZURE +``` + +**Integration mechanisms**: +- **GitHub**: OAuth flow via redirect to GitHub Apps +- **GitLab/Bitbucket/Azure**: Custom Server-Sent Events (SSE) installation flow with real-time progress streaming + +Key files: +- `src/api/git.api.ts` - Git repository operations (vendor-agnostic) +- `src/api/azure.api.ts` - Azure-specific PAT authentication +- `src/utils/EventSourceWithAuth.ts` - Custom SSE client with Bearer token support + +#### 2. Long-Running Operations (LRO) Pattern +Documentation generation is asynchronous and may take minutes. The app uses a job-based system: + +```typescript +// Workflow: +// 1. POST /v1/git/job/{genType}/start → returns lroJobId +// 2. Poll GET /v1/git/lro/jobs/{jobId} → check LroJobState +// 3. Stream progress via GET /v1/git/lro/jobs/{jobId}/logs (SSE) +// 4. On COMPLETE → fetch generated results +``` + +States: `START | VALIDATING | QUEUED | RUNNING | COMPLETE | FAILED | CANCELLED | TIMEOUT` + +Key files: +- `src/api/lro.api.ts` - Job status polling +- `src/api/docgen.api.ts` - Documentation generation triggers +- `src/components/dashboard/DashboardTerminal/` - Real-time progress UI + +#### 3. Layered Architecture +The codebase follows strict separation of concerns: + +``` +┌─────────────────────────────────────┐ +│ Components & Pages (UI Layer) │ +│ - React components │ +│ - styled-components │ +│ - Ant Design integration │ +└─────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Redux Store (State Layer) │ +│ - userSlice, authSlice, themeSlice │ +│ - Async thunks for API calls │ +│ - localStorage persistence │ +└─────────────┬───────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ API Layer (Data Layer) │ +│ - src/api/*.api.ts files │ +│ - All HTTP communication │ +│ - Request/response transformation │ +└─────────────────────────────────────┘ +``` + +**Critical rule**: Components NEVER import API modules directly. All data fetching flows through Redux thunks or React hooks that encapsulate API calls. + +#### 4. Authentication & Request Flow + +```typescript +// All HTTP requests flow through src/api/http.api.ts +const httpApi = axios.create({ baseURL: VITE_BASE_URL }) + +// Request interceptor: Adds Authorization header from localStorage +httpApi.interceptors.request.use(config => { + const token = readToken() + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +// Response interceptor: Handles 511 (auth required) → auto-logout +httpApi.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 511) { + // Logout user, clear token, redirect to / + } + throw error + } +) +``` + +Token persistence: `src/services/localStorage.service.ts` handles sync between Redux and localStorage. + +#### 5. Multi-Language Documentation Support +The platform supports language-specific documentation styles: + +```typescript +// Python: GOOGLE | EPYDOC | NUMPYDOC | REST +// JavaScript/TypeScript: JSDoc +// Java: JavaDoc +// C#: XML +// Kotlin: KDoc +// C/C++: Doxygen | JavaDoc-Style +``` + +Fetched via `getLanguage(orgName, repoName, vendor)` in `src/api/docgen.api.ts`, then passed to generation API. + +#### 6. Routing & Code Splitting +Routes defined in `src/components/router/AppRouter.tsx`: + +**Protected routes** (requires auth token): +- `/repositories` - Repository list dashboard +- `/repositories/{vendor}/{orgName}/{repoName}` - Repository detail +- `/repositories/{vendor}/{orgName}/{repoName}/analyze` - Analysis view +- `/repositories/{vendor}/{orgName}/{repoName}/docufy` - Documentation generation +- `/repositories/{vendor}/{orgName}/{repoName}/security` - Security scan +- `/jobs` - Job history +- `/penify-api-keys` - API key management +- `/profile/*` - User settings + +**Public routes**: +- `/auth/login`, `/auth/sign-up` - Authentication +- `/oauth/github`, `/oauth/google` - OAuth callbacks + +**Code splitting**: Most pages are lazy-loaded via `React.lazy()` + `withLoading()` HOC (except auth pages to prevent flickering). + +#### 7. Analytics Integration +Dual tracking system: + +```typescript +// Mixpanel: Custom event tracking with user context +trackEvent(eventName, properties) +trackPageView(pathname, user) +trackClicks() // Global click listener + +// Google Analytics 4: Basic page view tracking +trackGAPageView(pathname) +``` + +Initialized in `src/App.tsx`, configured in `src/config/mixpanel.ts` and `src/config/ga.ts`. + +All events include user metadata: `distinct_id`, `user_email`, `github_id`, `google_id`, etc. + +#### 8. Theme System +Dual theme support (light/dark) with system preference detection: + +```typescript +// src/styles/themes/themeVariables.ts defines color palettes +// useThemeWatcher() hook detects OS dark mode preference +// themeSlice stores user's explicit theme choice +// styled-components ThemeProvider propagates theme to all components +``` + +Theme toggle available in user profile settings. + +### Key Directory Purposes + +**`src/api/`** - All backend HTTP communication +Each file maps to a backend service domain (auth, git, docgen, payment, etc.). All functions return typed Promises. + +**`src/store/`** - Redux Toolkit configuration +- `store.ts` - Store setup with error logging middleware +- `slices/` - Feature-based reducers with async thunks + +**`src/components/`** - Feature-based React components +Organized by domain (dashboard, auth, profile, common). Each complex component has its own directory with `.styles.ts` file. + +**`src/pages/`** - Route-level page components +Top-level components that map to router paths. These orchestrate smaller components. + +**`src/domain/`** - Data models +TypeScript interfaces representing backend entities (UserModel, etc.). + +**`src/hooks/`** - Custom React hooks +`reduxHooks.ts` provides typed `useAppSelector`/`useAppDispatch`. Other hooks encapsulate common logic (language switching, responsive breakpoints). + +**`src/utils/`** - Utility functions +`EventSourceWithAuth.ts` is critical - custom EventSource class that supports Authorization headers for SSE streaming. + +**`src/constants/`** - Enums and static config +Plan types, Git vendors, language codes, etc. + +**`src/styles/`** - Global styles and theme definitions +`GlobalStyle.tsx` applies CSS reset + base styles. `themeVariables.ts` defines color system. + +**`src/locales/`** - i18n translation files +Currently supports English (en) and German (de). + +### Important Implementation Notes + +1. **Backend uses snake_case, frontend uses camelCase** + API transformation happens in Redux thunks or API layer. Example in `src/api/auth.api.ts`. + +2. **Environment variables required**: + ```bash + VITE_BASE_URL= + VITE_ASSETS_BUCKET= + ``` + +3. **Path alias**: `@app/*` resolves to `src/*` (configured in tsconfig + vite) + +4. **SVG imports**: SVGs can be imported as React components via vite-plugin-svgr + +5. **Vendor colors**: Each Git platform has a brand color (GitHub: purple, GitLab: orange, etc.) defined in theme variables + +6. **Modal/Notification controllers**: Centralized UI feedback in `src/controllers/` + ```typescript + import { notificationController } from '@app/controllers/notificationController' + notificationController.success({ message: 'Success!' }) + ``` + +7. **Subscription tiers**: FREE, PREMIUM, PRO (impacts feature availability) + User plan stored in `user.subscription_info.planType` + +8. **Security**: Never commit `.env` files. API keys/tokens only in environment variables. + +### Common Workflows + +**Adding a new API endpoint**: +1. Define TypeScript interface for request/response in `src/@types/` or inline +2. Add function to appropriate `src/api/*.api.ts` file +3. Create async thunk in Redux slice if state management needed +4. Use in component via `useAppDispatch()` + +**Adding a new protected route**: +1. Create page component in `src/pages/DashboardPages/` +2. Lazy load with `withLoading()` HOC +3. Add route in `src/components/router/AppRouter.tsx` inside `` +4. Add navigation link in `src/components/layouts/main/MainSider/` + +**Adding analytics tracking**: +```typescript +import { trackEvent } from '@app/config/mixpanel' + +// In component: +trackEvent('Button Clicked', { buttonName: 'Generate Docs', repoName }) +``` + +**Styling new components**: +```typescript +// ComponentName.tsx +import * as S from './ComponentName.styles' + +export const ComponentName = () => ... + +// ComponentName.styles.ts +import styled from 'styled-components' + +export const Container = styled.div` + color: ${props => props.theme.colors.main.primary}; +` +``` diff --git a/package.json b/package.json index 2e32981b..39589470 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "framer-motion": "^12.5.0", "html-react-parser": "^5.2.3", "i18next": "^23.15.1", - "mixpanel-browser": "^2.55.1", + "mixpanel-browser": "^2.64.0", "prismjs": "^1.29.0", "qs": "^6.13.0", "react": "^18.3.1", diff --git a/src/api/docgen.api.ts b/src/api/docgen.api.ts index 7c1d846d..76f415e3 100644 --- a/src/api/docgen.api.ts +++ b/src/api/docgen.api.ts @@ -108,7 +108,7 @@ export const triggerDocGen = ( archDocConfig?: ArchDocConfig, ): Promise => { const data = buildDocGenData(orgName, repoName, vendor); - const url = `/v1/git/generate/${genType}/doc`; + const url = `/v1/git/job/${genType}/start`; return httpApi.post(url, { ...data, style_guide: styleGuide, arch_doc_config: archDocConfig }).then(({ data }) => data); }; @@ -119,15 +119,14 @@ export const getDocGenStatus = ( genType: string, lroJobId: string, ): Promise => { - const data = buildDocGenData(orgName, repoName, vendor); - const url = `/v1/git/generate/${genType}/doc/status?jobId=${lroJobId}`; - return httpApi.post(url, { ...data }).then(({ data }) => data); + const url = `/v1/git/job/${lroJobId}/status`; + return httpApi.get(url).then(({ data }) => data); }; export const terminateJob = ( lroJobId: string, ): Promise> => { - const url = `/v1/git/lro/${lroJobId}/terminate`; + const url = `/v1/git/job/${lroJobId}/terminate`; return httpApi.post>(url).then(({ data }) => data); }; @@ -165,7 +164,7 @@ export const getAdvancedRepoAnalysisDetails = ( analysisType: string, ): Promise> => { const data = buildDocGenData(orgName, repoName, vendor); - const url = `/v1/git/analyze/repo/${analysisType}/details`; + const url = `/v1/git/job/${analysisType}/details`; return httpApi.post>(url, { ...data }).then(({ data }) => data); // Extract the message }; diff --git a/src/api/git.api.ts b/src/api/git.api.ts index 633beb90..d7cd8c3b 100644 --- a/src/api/git.api.ts +++ b/src/api/git.api.ts @@ -49,12 +49,6 @@ export enum FunctionOutdatedStatusEnum { CURRENT = "CURRENT" } -export type FunctionOutdatedResponseSchema = { - status: FunctionOutdatedStatusEnum; - explanation: string; - docstring?: string; - grammarError?: boolean; -} export type FunctionDetail = { name: string; @@ -69,10 +63,14 @@ export type FunctionDetail = { halsteadVolume?: number; halsteadDifficulty?: number; halsteadEffort?: number; + grammarError: boolean; + funStartLine: number; + funEndLine: number; + suggestedDocstring?: string; complexityCategory?: ComplexityCategory; - languageType: string; - outdatedStatus?: FunctionOutdatedResponseSchema; + plName: string; + outdatedDocstring?: boolean; urgentDocumentation?: boolean; hasDocstring: boolean; @@ -101,6 +99,14 @@ export type TextStatistics = { wordCount?: number; readmeScore?: number; } + +export type LLMReadmeAnalysisResponse = { + overallEvaluation: string[]; + strengths: string[]; + weaknesses: string[]; + recommendations: string[]; + score: number; // LLM interpretation score +} export type ReadmeAnalysis = { lines: number; @@ -110,7 +116,7 @@ export type ReadmeAnalysis = { url: string; brokenLinks: string[]; grammarError: number; - llmInterpretationScore: number; + llmInterpretation: LLMReadmeAnalysisResponse; calculatedQualityScore: number; } @@ -126,6 +132,11 @@ export type GitAppMetaData = { analysisReport: AnalysisReport; }; +export type GitAppAdvancedMetaData = { + analysisReport: AnalysisReport; +}; + + export enum PlanType { FREE = "FREE", PREMIUM = "PREMIUM", @@ -140,11 +151,11 @@ export type GitAppUsageType = { vendor: string; planType: PlanType; gitAppMetaData: GitAppMetaData; + gitAppAdvancedMeta: GitAppAdvancedMetaData; isActive: boolean; isPrivate: boolean; isInstalled: boolean; userId: number; - full_repo_count: number; }; export const getGitAppUsageForRepository = ( diff --git a/src/components/common/BaseMenu/BaseMenu.styles.ts b/src/components/common/BaseMenu/BaseMenu.styles.ts index b789bc81..984120c8 100644 --- a/src/components/common/BaseMenu/BaseMenu.styles.ts +++ b/src/components/common/BaseMenu/BaseMenu.styles.ts @@ -3,14 +3,14 @@ import { Menu as AntMenu } from 'antd'; export const Menu = styled(AntMenu)` &.ant-menu .ant-menu-item-icon { - font-size: ${({ theme }) => theme.fontSizes.xl}; - width: 1.25rem; + font-size: ${({ theme }) => theme.fontSizes.md}; + width: 1rem; } .ant-menu-item, .ant-menu-submenu { font-size: ${({ theme }) => theme.fontSizes.xs}; - border-radius: 0; + border-radius: 6px; } .ant-menu-item { diff --git a/src/components/dashboard/DashboardHeader/OutdatedDocsModal.tsx b/src/components/dashboard/DashboardHeader/OutdatedDocsModal.tsx index 53a21046..b72156b4 100644 --- a/src/components/dashboard/DashboardHeader/OutdatedDocsModal.tsx +++ b/src/components/dashboard/DashboardHeader/OutdatedDocsModal.tsx @@ -76,6 +76,27 @@ const SuggestedDocBlock = styled.pre` word-break: break-all; `; +const HoverLink = styled.div` + display: none; + position: absolute; + background: #f0f0f0; + border: 1px solid #d9d9d9; + padding: 4px 8px; + border-radius: 4px; + z-index: 1000; + margin-top: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + font-size: 12px; + + .link-container:hover & { + display: block; + } +`; + +const LinkContainer = styled.div` + position: relative; +`; + const OutdatedDocsModal: React.FC = ({ visible, onClose, @@ -88,7 +109,7 @@ const OutdatedDocsModal: React.FC = ({ // Filter for outdated and urgent docs const outdatedFunctions = functionDetails?.filter( - func => func.outdatedStatus?.status === FunctionOutdatedStatusEnum.OUTDATED + func => func.outdatedDocstring ) || []; const urgentFunctions = functionDetails?.filter( @@ -134,8 +155,9 @@ const OutdatedDocsModal: React.FC = ({ // Prepare data for charts // Outdated by language chart data + console.log('Outdated Functions:', outdatedFunctions); const outdatedByLanguage = outdatedFunctions.reduce((acc, func) => { - const lang = func.languageType; + const lang = func.plName; acc[lang] = (acc[lang] || 0) + 1; return acc; }, {} as Record); @@ -236,14 +258,26 @@ const OutdatedDocsModal: React.FC = ({ const outdatedColumns = [ { title: 'Function Name', - dataIndex: 'name', - key: 'name', + dataIndex: 'methodName', + key: 'methodName', render: (text: string, record: FunctionDetail) => ( - - {text} + + + {text} +
{record.fileName} -
+ + + {record.gitLocation} + + + ), }, { @@ -253,7 +287,7 @@ const OutdatedDocsModal: React.FC = ({ render: (text: ComplexityCategory, record: FunctionDetail) => { const color = text === ComplexityCategory.HIGH ? 'red' : text === ComplexityCategory.MEDIUM ? 'orange' : 'green'; - return {text}; + return <>{text} ; }, sorter: (a: FunctionDetail, b: FunctionDetail) => { const order = { [ComplexityCategory.HIGH]: 3, [ComplexityCategory.MEDIUM]: 2, [ComplexityCategory.LOW]: 1 }; @@ -273,25 +307,40 @@ const OutdatedDocsModal: React.FC = ({ }, { title: 'Language', - dataIndex: 'languageType', + dataIndex: 'plName', key: 'language', render: (text: string) => {text} }, { title: 'Issue', - dataIndex: 'outdatedStatus', + dataIndex: 'outdatedDocstring', key: 'issue', - render: (status: FunctionDetail['outdatedStatus']) => { + render: (status: FunctionDetail['outdatedDocstring'], record: FunctionDetail) => { if (!status) return '-'; return ( - + } color="warning"> - {status.grammarError ? 'Grammar Error' : 'Outdated'} + {record.grammarError ? 'Grammar Error' : 'Outdated'} ); } }, + // { + // title: 'CP', + // dataIndex: 'cyclomaticComplexity', + // key: 'cyclomaticComplexity', + // render: (status: FunctionDetail['cyclomaticComplexity'], record: FunctionDetail) => { + // if (!status) return '-'; + // return ( + // + // } > + // {record.grammarError ? 'Grammar Error' : 'Outdated'} + // + // + // ); + // } + // }, { title: 'Urgent', dataIndex: 'urgentDocumentation', @@ -377,24 +426,16 @@ const OutdatedDocsModal: React.FC = ({ )} - {record.outdatedStatus?.docstring && ( + {record.suggestedDocstring && ( <> Suggested Docstring: - {record.outdatedStatus.docstring} + {record.suggestedDocstring} )} Function Code: {record.content} - - {record.outdatedStatus?.explanation && ( - <> - Issue Explanation: - - {record.outdatedStatus.explanation} - - - )} + ), }} @@ -418,18 +459,26 @@ const OutdatedDocsModal: React.FC = ({ {record.docstring} )} + + {record.suggestedDocstring && ( + <> + Suggested Docstring: + {record.suggestedDocstring} + + )} + Function Code: {record.content} - {record.outdatedStatus?.explanation && ( + {/* {record.suggestedDocstring && ( <> Issue Explanation: - {record.outdatedStatus.explanation} + {record.suggestedDocstring} - )} + )} */} ), }} diff --git a/src/components/dashboard/DashboardHeader/ReadmeAnalysisModal.tsx b/src/components/dashboard/DashboardHeader/ReadmeAnalysisModal.tsx index eda81307..7f64a20d 100644 --- a/src/components/dashboard/DashboardHeader/ReadmeAnalysisModal.tsx +++ b/src/components/dashboard/DashboardHeader/ReadmeAnalysisModal.tsx @@ -1,13 +1,10 @@ import React, { useState } from 'react'; -import { Modal, Typography, Row, Col, Card, Table, Tag, Alert, Progress, Statistic, Divider, Tooltip, Button, Tabs } from 'antd'; -import { ReadmeAnalysis, TextStatistics } from '@app/api/git.api'; -import { BookOutlined, LinkOutlined, WarningOutlined, ClockCircleOutlined, FileTextOutlined, CheckCircleOutlined, CloseCircleOutlined, BilibiliFilled, FileWordOutlined } from '@ant-design/icons'; +import { Modal, Typography, Row, Col, Card, Alert, Progress, Tabs, Tooltip, Badge } from 'antd'; +import { ReadmeAnalysis } from '@app/api/git.api'; +import { LinkOutlined, WarningOutlined, ClockCircleOutlined, FileTextOutlined, CheckCircleOutlined, InfoCircleOutlined } from '@ant-design/icons'; import styled from 'styled-components'; -import ReactECharts from 'echarts-for-react'; -import { theme } from './RepoMetricsCards.styles'; -import MarkdownRenderer from './MarkdownRenderer'; -const { Title, Paragraph, Text } = Typography; +const { Text } = Typography; const { TabPane } = Tabs; interface ReadmeAnalysisModalProps { @@ -16,27 +13,208 @@ interface ReadmeAnalysisModalProps { readmeAnalysis?: ReadmeAnalysis; repoName?: string; orgName?: string; - readmeSuggestion?: string; } +// Enterprise-grade styled components +const EnterpriseModal = styled(Modal)` + + .ant-modal-content { + padding: 10px 12px; + border-radius: 12px; + box-shadow: 0 20px 64px rgba(0, 0, 0, 0.15), 0 8px 32px rgba(0, 0, 0, 0.08); + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.8); + } + + .ant-modal-header { + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border-bottom: 1px solid #e2e8f0; + padding: 12px 16px; + + .ant-modal-title { + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-size: 16px; + font-weight: 600; + color: #1e293b; + letter-spacing: -0.01em; + line-height: 1.3; + } + } + + .ant-modal-body { + background: #ffffff; + } + + .ant-modal-close { + top: 10px; + right: 20px; + + .ant-modal-close-x { + width: 46px; + height: 46px; + line-height: 46px; + font-size: 16px; + color: #64748b; + transition: all 0.2s ease; + border-radius: 8px; + + &:hover { + color: #1e293b; + background: rgba(0, 0, 0, 0.04); + } + } + } +`; + const StyledCard = styled(Card)` - border-radius: 8px; - margin-bottom: 16px; + border-radius: 12px; + border: 1px solid #e2e8f0; + margin-bottom: 12px; height: 100%; - box-shadow: ${theme.shadows.card}; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.08); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: #ffffff; + + .ant-card-head { + border-bottom: 1px solid #f1f5f9; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border-radius: 12px 12px 0 0; + padding: 10px 16px; + min-height: 40px; + + .ant-card-head-title { + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + font-weight: 600; + color: #1e293b; + letter-spacing: -0.01em; + padding: 0; + } + } + + .ant-card-body { + padding: 12px; + } `; -const StatCard = styled(StyledCard)` - .ant-statistic-title { - font-size: 14px; +const MetricRow = styled.div` + display: flex; + align-items: center; + padding: 6px 0; + border-bottom: 1px solid #f1f5f9; + + &:last-child { + border-bottom: none; + padding-bottom: 0; } - .ant-statistic-content { - font-size: 22px; + + &:first-child { + padding-top: 0; } `; -const MetricProgress = styled(Progress)` +const MetricLabel = styled.div` + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-size: 13px; + font-weight: 500; + color: #64748b; + flex: 1; + display: flex; + align-items: center; +`; + +const MetricValue = styled.div` + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + font-weight: 600; + color: #1e293b; + text-align: right; + display: flex; + align-items: center; + gap: 6px; +`; + +const MetricGrid = styled(Row)` + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 8px; + padding: 0 2px; +`; + +const MetricGridItem = styled.div` + padding: 8px; + border-radius: 8px; + background: #f8fafc; + display: flex; + flex-direction: column; + align-items: center; + + .metric-label { + font-size: 12px; + color: #64748b; + margin-bottom: 4px; + text-align: center; + } + + .metric-value { + font-size: 16px; + font-weight: 600; + color: #1e293b; + display: flex; + align-items: center; + gap: 4px; + } +`; + +const QualityAlert = styled(Alert)` + border-radius: 8px; + border: none; margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + + .ant-alert-message { + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-weight: 600; + font-size: 15px; + } + + .ant-alert-description { + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + margin-top: 6px; + } +`; + +const TabsContainer = styled(Tabs)` + .ant-tabs-nav { + margin-bottom: 12px; + } + + .ant-tabs-tab { + font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-weight: 500; + font-size: 14px; + padding: 6px 12px; + } + + .ant-tabs-tab-active .ant-tabs-tab-btn { + color: #3b82f6 !important; + font-weight: 600; + } +`; + +const ScoreBadge = styled(Badge)` + .ant-badge-count { + background-color: ${props => props.color || '#3b82f6'}; + font-size: 13px; + font-weight: 600; + height: 22px; + line-height: 22px; + padding: 0 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } `; const ReadmeAnalysisModal: React.FC = ({ @@ -44,8 +222,7 @@ const ReadmeAnalysisModal: React.FC = ({ onClose, readmeAnalysis, repoName, - orgName, - readmeSuggestion + orgName }) => { const [activeTab, setActiveTab] = useState('1'); @@ -78,275 +255,440 @@ const ReadmeAnalysisModal: React.FC = ({ if (score >= 50) return { text: 'Average', color: '#faad14' }; return { text: 'Needs Improvement', color: '#f5222d' }; }; + + // Get color for score values + const getScoreColor = (score: number, threshold1: number, threshold2: number, inverse = false) => { + if (!inverse) { + if (score >= threshold1) return '#52c41a'; + if (score >= threshold2) return '#faad14'; + return '#f5222d'; + } else { + if (score <= threshold2) return '#52c41a'; + if (score <= threshold1) return '#faad14'; + return '#f5222d'; + } + }; const qualityLevel = getQualityLevel(qualityScore); - - // Chart options for the radar chart - const radarChartOption = { - tooltip: { - trigger: 'item' - }, - radar: { - indicator: [ - { name: 'Readability', max: 100 }, - { name: 'Length', max: 100 }, - { name: 'Structure', max: 100 }, - { name: 'Grammar', max: 100 }, - { name: 'Links', max: 100 } - ] - }, - series: [ - { - name: 'README Quality Metrics', - type: 'radar', - data: [ - { - value: [ - // Convert various metrics to a 0-100 scale - Math.min(100, ((textStats.fleschReadingEase || 0) / 100) * 100), - Math.min(100, ((readmeAnalysis.wordCount || 0) / 1000) * 100), - Math.min(100, ((readmeAnalysis.llmInterpretationScore || 0))), - Math.min(100, 100 - ((readmeAnalysis.grammarError || 0) / 10) * 100), - Math.min(100, 100 - ((readmeAnalysis.brokenLinks || 0) / 5) * 100) - ], - name: 'Score', - areaStyle: { - color: 'rgba(24, 144, 255, 0.3)' - } - } - ] - } - ] - }; return ( - - + - - - README Quality: {qualityLevel.text}} - description={ -
- `${qualityScore.toFixed(1)}%`} - /> - - This score is based on readability, completeness, structure, and other factors. - + {/* Quality Score Header */} + + + + README Quality: {qualityLevel.text} +
} + description={ + + } type={qualityScore >= 70 ? "success" : qualityScore >= 50 ? "warning" : "error"} showIcon /> - - - - } - suffix={`words`} - /> - - } - suffix={`lines`} - /> - - } - suffix={`seconds`} - /> - - - - - - } - /> - - } - valueStyle={{ color: readmeAnalysis.brokenLinks.length > 0 ? '#f5222d' : '#52c41a' }} - /> - - } - valueStyle={{ color: readmeAnalysis.grammarError > 0 ? '#f5222d' : '#52c41a' }} - /> - - - - - -
- LLM Interpretation Score: - {(readmeAnalysis.llmInterpretationScore*10).toFixed(1)}% - - = 70 ? '#52c41a' : '#faad14'} - showInfo={false} - /> - -
-
- Flesch Reading Ease: - {(readmeAnalysis.textStatistics.fleschReadingEase||0).toFixed(1)}% - - = 70 ? '#52c41a' : '#faad14'} - showInfo={false} - /> - + +
+
LLM Score
+
+ {((readmeAnalysis.llmInterpretation?.score || 0) * 10).toFixed(1)}/10
-
- Flesch-Kincaid Grade: - {(readmeAnalysis.textStatistics.fleschKincaidGrade||0).toFixed(1)}% - - = 70 ? '#52c41a' : '#faad14'} - showInfo={false} - /> - -
-
- SMOG Index: - {(readmeAnalysis.textStatistics.smogIndex||0).toFixed(1)}% - - = 70 ? '#52c41a' : '#faad14'} - showInfo={false} - /> - -
- +
- - - - - {qualityScore < 70 && ( - } - style={{ marginBottom: 8 }} - /> + + + {/* Dashboard Layout */} + + {/* Left Column */} + + {/* Key Metrics */} + + + +
Word Count
+
+ + {readmeAnalysis.textStatistics?.wordCount || 0} +
+
+ + +
Lines
+
+ {textStats.lines || 0} +
+
+ + +
Reading Time
+
+ + {Math.round(textStats.readingTime || 0)}s +
+
+ + +
Flesch Reading Ease
+
+ {Math.round(textStats.fleschReadingEase || 0)} +
+
+ + +
Flesch Grade Level
+
+ {Math.round(textStats.fleschKincaidGrade || 0)} +
+
+ + +
Issues
+
0 || readmeAnalysis.grammarError > 0) + ? '#f5222d' + : '#52c41a' + }}> + {readmeAnalysis.brokenLinks.length + readmeAnalysis.grammarError} +
+
+
+
+ + + + {/* Content Issues */} + {(readmeAnalysis.brokenLinks.length > 0 || readmeAnalysis.grammarError > 0) && ( + +
+ {readmeAnalysis.brokenLinks.length > 0 && ( + + + + Broken Links + + + {readmeAnalysis.brokenLinks.length} + + )} - {readmeAnalysis.brokenLinks > 0 && ( - 0 && ( + + + + Grammar Issues + + + {readmeAnalysis.grammarError} + + + )} +
+
+ )} + + + {/* Right Column */} + + + AI Assessment + + + + + } + style={{ height: '100%' }} + > + {qualityScore >= 80 ? ( + } + /> + ) : ( +
+ {readmeAnalysis.brokenLinks.length > 0 && ( + 1 ? 's' : ''} found. Update or remove them to improve reliability.`} type="warning" showIcon icon={} - style={{ marginBottom: 8 }} /> )} {readmeAnalysis.grammarError > 0 && ( - 1 ? 's' : ''} detected. Consider proofreading for better readability.`} type="warning" showIcon icon={} - style={{ marginBottom: 8 }} /> )} - {readmeAnalysis.wordCount < 300 && ( - )} - {readmeAnalysis.llmInterpretationScore < 70 && ( - )} - {qualityScore >= 80 && ( - } /> )} - - +
+ )} + + {/* LLM Interpretation Section */} + {readmeAnalysis.llmInterpretation && ( +
+ + + {readmeAnalysis.llmInterpretation.overallEvaluation.length > 0 && ( +
+
    + {readmeAnalysis.llmInterpretation.overallEvaluation.map((strength, i) => ( +
  • {strength}
  • + ))} +
+
+ )} +
+ )}
- - {readmeSuggestion ? ( + + + {readmeAnalysis.llmInterpretation ? (
- - + + {readmeAnalysis.llmInterpretation.overall_evaluation && ( + +
+ {readmeAnalysis.llmInterpretation.overall_evaluation} +
+
+ )} + + + {readmeAnalysis.llmInterpretation.strengths && readmeAnalysis.llmInterpretation.strengths.length > 0 && ( + + + + Strengths + + } + bodyStyle={{ padding: '12px' }} + style={{ height: '100%', borderTop: '3px solid #16a34a' }} + > +
    + {Array.isArray(readmeAnalysis.llmInterpretation.strengths) ? + readmeAnalysis.llmInterpretation.strengths.map((strength, i) => ( +
  • + {strength} +
  • + )) : +
  • + {readmeAnalysis.llmInterpretation.strengths} +
  • + } +
+
+ + )} + + {readmeAnalysis.llmInterpretation.weaknesses && readmeAnalysis.llmInterpretation.weaknesses.length > 0 && ( + + + + Areas to Improve + + } + bodyStyle={{ padding: '12px' }} + style={{ height: '100%', borderTop: '3px solid #dc2626' }} + > +
    + {Array.isArray(readmeAnalysis.llmInterpretation.weaknesses) ? + readmeAnalysis.llmInterpretation.weaknesses.map((weakness, i) => ( +
  • + {weakness} +
  • + )) : +
  • + {readmeAnalysis.llmInterpretation.weaknesses} +
  • + } +
+
+ + )} +
+ + {readmeAnalysis.llmInterpretation.recommendations && ( + + + Detailed Recommendations + + } + bodyStyle={{ padding: '16px' }} + style={{ marginTop: '16px', borderTop: '3px solid #0369a1' }} + > +
+ {Array.isArray(readmeAnalysis.llmInterpretation.recommendations) ? ( +
    + {readmeAnalysis.llmInterpretation.recommendations.map((recommendation, i) => ( +
  • + {recommendation} +
  • + ))} +
+ ) : ( +
+ {readmeAnalysis.llmInterpretation.recommendations} +
+ )} +
+
+ )}
) : ( - )}
- - + + ); }; diff --git a/src/components/dashboard/DashboardHeader/RepoMetricsCards.tsx b/src/components/dashboard/DashboardHeader/RepoMetricsCards.tsx index 4d442442..eea2076b 100644 --- a/src/components/dashboard/DashboardHeader/RepoMetricsCards.tsx +++ b/src/components/dashboard/DashboardHeader/RepoMetricsCards.tsx @@ -1,16 +1,14 @@ -import React, { useEffect, useState } from 'react'; -import { Row, Col, Typography, Tooltip, Progress, Button } from 'antd'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Row, Col, Typography, Tooltip, Progress } from 'antd'; import { FileOutlined, WarningOutlined, BookOutlined, - ScanOutlined, LoadingOutlined, - ReloadOutlined, CodeOutlined } from '@ant-design/icons'; -import { AnalysisReport, BasicFunctionDocAnalysis, GitAppUsageType, FunctionDetail } from '@app/api/git.api'; -import { triggerAnalysisRepo, getAdvancedRepoAnalysisDetails, getAnalysisRepoStatus, terminateJob } from '@app/api/docgen.api'; +import { AnalysisReport, BasicFunctionDocAnalysis, GitAppUsageType, FunctionDetail, getGitApp } from '@app/api/git.api'; +import { triggerDocGen, getAdvancedRepoAnalysisDetails, getDocGenStatus, terminateJob } from '@app/api/docgen.api'; import DocumentationMetricsModal from './DocumentationMetricsModal'; import { checkAzurePAT } from '@app/api/azure.api'; import AzurePATModal from '../AzurePATModal/AzurePATModal'; @@ -23,7 +21,6 @@ import { MetricCard, NoAnalysisContent, IconWrapper, - RefreshButton, MetricValue, MetricLabel, AnalysisPlaceholder, @@ -38,13 +35,6 @@ const { Text } = Typography; interface RepoMetricsCardsProps { repoDetails: GitAppUsageType; - documentationStats: { - totalFunctions: number; - documentedFunctions: number; - outdatedDocs: number; - readmeScore: number; - codeCoverage: number; - }; onRunFullAnalysis?: (func: () => void) => void; // Change to pass the function instead } @@ -53,9 +43,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF const [hasAdvancedRepoAnalysis, setHasAdvancedRepoAnalysis] = useState(false); const [hasReadMeAnalysis, setHasReadmeAnalysis] = useState(false); - const [isRepoAnalyzing, setIsRepoAnalyzing] = useState(false); - const [isAdvancedRepoAnalyzing, setIsAdvancedRepoAnalyzing] = useState(false); - const [isReadMeAnalyzing, setIsReadMeAnalyzing] = useState(false); + const [isDocQualityAnalyzing, setIsDocQualityAnalyzing] = useState(false); const [progress, setProgress] = useState(0); // State for documentation metrics modal @@ -68,7 +56,6 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF const [isReadmeModalVisible, setIsReadmeModalVisible] = useState(false); const [functionDetails, setFunctionDetails] = useState([]); - const [readMeDetails, setReadMeDetails] = useState(""); const [loadingFunctionDetails, setLoadingFunctionDetails] = useState(false); // State for terminal logs modal @@ -78,11 +65,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF // Azure PAT modal state const [azurePATModalVisible, setAzurePATModalVisible] = useState(false); - const [pendingAnalysis, setPendingAnalysis] = useState<{ - setIsAnalyzing: React.Dispatch>; - hasReportAnalysis: React.Dispatch>; - analysisType?: string; - } | null>(null); + const [resumeAfterPat, setResumeAfterPat] = useState(false); // Use state for metrics instead of variables const [totalFunctions, setTotalFunctions] = useState(-1); @@ -93,6 +76,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF const [codeCoverage, setCodeCoverage] = useState(-1); const { repoName, organizationName: orgName } = repoDetails; const [lroJobId, setLroJobId] = useState(''); + const completionHandledRef = useRef(false); // Basic function doc analysis from repo details const [basicFuncDocAnalysis, setBasicFuncDocAnalysis] = useState(undefined); @@ -101,7 +85,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF const [programmingLanguages, setProgrammingLanguages] = useState>({}); // Add a message to the terminal - const addTerminalMessage = (content: string, type: MessageType = MessageType.INFO) => { + const addTerminalMessage = useCallback((content: string, type: MessageType = MessageType.INFO) => { const newMessage: TerminalMessage = { content, type, @@ -110,12 +94,12 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF }; setTerminalMessages(prev => [...prev, newMessage]); - }; + }, []); // Clear terminal messages - const clearTerminalMessages = () => { + const clearTerminalMessages = useCallback(() => { setTerminalMessages([]); - }; + }, []); // Function to update metrics from analysis report const updateMetricsFromReport = (report: AnalysisReport) => { @@ -135,6 +119,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF if(report.readmeAnalysis) { + console.log('Readme analysis found:', report.readmeAnalysis); setReadmeScore(report.readmeAnalysis.calculatedQualityScore); } @@ -146,11 +131,14 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF }; useEffect(() => { - if (repoDetails?.gitAppMetaData?.analysisReport) { - const report = repoDetails.gitAppMetaData.analysisReport; + console.log('RepoMetricsCards mounted with repoDetails:', repoDetails.organizationName); + if (repoDetails?.gitAppAdvancedMeta?.analysisReport) { + console.log('Basic function doc analysis found11111:'); + const report = repoDetails.gitAppAdvancedMeta.analysisReport; updateMetricsFromReport(report); if (report.basicFuncDocAnalysis?.totalFunctions > 0) { + console.log('Basic function doc analysis found:', report.basicFuncDocAnalysis); setHasRepoAnalysis(true); } @@ -165,69 +153,83 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF } }, [repoDetails]); + useEffect(() => { + completionHandledRef.current = false; + }, [lroJobId]); + const documentationPercentage = Math.round((documentedFunctions / totalFunctions) * 100) || 0; - const runAnalysis = async (setIsAnalyzing: React.Dispatch>, hasReportAnalysis: React.Dispatch>, analysisType?: string) => { - setIsAnalyzing(true); - hasReportAnalysis(false); - const orgName = repoDetails?.organizationName; - const repoName = repoDetails?.repoName; - const vendor = repoDetails?.vendor; - - // Set current analysis type and show terminal - setCurrentAnalysisType(analysisType || "coverage"); + const runDocQualityAnalysis = useCallback(async () => { + if (!repoDetails?.organizationName || !repoDetails?.repoName) return; + if (isDocQualityAnalyzing) { + setIsTerminalVisible(true); + return; + } + + setIsDocQualityAnalyzing(true); + setProgress(0); + setLroJobId(''); + + setCurrentAnalysisType('docQuality'); setIsTerminalVisible(true); clearTerminalMessages(); - - // Add initial messages - addTerminalMessage(`Starting ${analysisType || "coverage"} analysis for ${orgName}/${repoName}`, MessageType.SYSTEM); - addTerminalMessage(`Repository: ${orgName}/${repoName}`, MessageType.INFO); - addTerminalMessage(`Analysis Type: ${analysisType || "coverage"}`, MessageType.INFO); + + addTerminalMessage( + `Starting doc-quality analysis for ${repoDetails.organizationName}/${repoDetails.repoName}`, + MessageType.SYSTEM, + ); + addTerminalMessage( + `Repository: ${repoDetails.organizationName}/${repoDetails.repoName}`, + MessageType.INFO, + ); + addTerminalMessage(`Analysis Type: docQuality`, MessageType.INFO); try { - // Check if this is an Azure DevOps repository - if (vendor === 'AZURE') { + if (repoDetails.vendor === 'AZUREDEVOPS') { addTerminalMessage(`Checking Azure DevOps credentials...`, MessageType.INFO); - try { - // Check if the Azure DevOps PAT is valid - const patResponse = await checkAzurePAT(); - + const patResponse = await checkAzurePAT(repoDetails.organizationName); if (!patResponse.isValid) { - // Store the current analysis request for resuming after PAT is provided - setPendingAnalysis({ - setIsAnalyzing, - hasReportAnalysis, - analysisType - }); - - // Show PAT input modal + setIsDocQualityAnalyzing(false); + setResumeAfterPat(true); setAzurePATModalVisible(true); - - addTerminalMessage(`Azure DevOps Personal Access Token required`, MessageType.WARNING); + addTerminalMessage( + `Azure DevOps Personal Access Token required`, + MessageType.WARNING, + ); return; } - addTerminalMessage(`Azure DevOps credentials validated`, MessageType.SUCCESS); } catch (patError) { console.error('Error checking Azure PAT:', patError); - addTerminalMessage(`Error checking Azure credentials: ${patError}`, MessageType.ERROR); - setIsAnalyzing(false); + addTerminalMessage( + `Error checking Azure credentials: ${patError}`, + MessageType.ERROR, + ); + setIsDocQualityAnalyzing(false); return; } } - + addTerminalMessage(`Triggering analysis...`, MessageType.INFO); - const response = await triggerAnalysisRepo(orgName, repoName, vendor, analysisType); + const response = await triggerDocGen( + repoDetails.organizationName, + repoDetails.repoName, + repoDetails.vendor, + 'docQuality', + ); setLroJobId(response.lroJobId); addTerminalMessage(`Analysis triggered successfully`, MessageType.SUCCESS); } catch (error) { console.error('Error triggering analysis:', error); - setIsAnalyzing(false); + setIsDocQualityAnalyzing(false); addTerminalMessage(`Error triggering analysis: ${error}`, MessageType.ERROR); - return; } - }; + }, [addTerminalMessage, clearTerminalMessages, isDocQualityAnalyzing, repoDetails]); + + useEffect(() => { + onRunFullAnalysis?.(runDocQualityAnalysis); + }, [onRunFullAnalysis, runDocQualityAnalysis]); // Function to fetch function details for the outdated docs modal const fetchFunctionDetails = async () => { @@ -258,48 +260,16 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF } }; - const fetchReadMeDetails = async () => { - if (!repoDetails?.organizationName || !repoDetails?.repoName || !repoDetails?.installationId) { - console.error('Missing repository information'); - return; - } - try { - setLoadingFunctionDetails(true); - addTerminalMessage(`Fetching README details for ${repoDetails.organizationName}/${repoDetails.repoName}`, MessageType.INFO); - let details = await getAdvancedRepoAnalysisDetails( - repoDetails.organizationName, - repoDetails.repoName, - repoDetails.vendor, - 'readme' - ); - details = details as Record; - if(details && details["content"]) { - setReadMeDetails(details["content"]); - } - // Add handling for suggestion - if(details && details["suggestion"]) { - setReadmeSuggestion(details["suggestion"]); - addTerminalMessage(`Successfully retrieved README suggestion`, MessageType.SUCCESS); - } - addTerminalMessage(`Successfully retrieved README details`, MessageType.SUCCESS); - } catch (error) { - console.error('Error fetching README details:', error); - addTerminalMessage(`Error fetching README details: ${error}`, MessageType.ERROR); - } finally { - setLoadingFunctionDetails(false); - } - }; - // Handler to open the documentation metrics modal const handleDocMetricsClick = () => { - if (hasRepoAnalysis && !isRepoAnalyzing) { + if (hasRepoAnalysis && !isDocQualityAnalyzing) { setIsDocMetricsModalVisible(true); } }; // Handler to open the outdated docs modal const handleOutdatedDocsClick = () => { - if (hasAdvancedRepoAnalysis && !isAdvancedRepoAnalyzing) { + if (hasAdvancedRepoAnalysis && !isDocQualityAnalyzing) { setIsOutdatedDocsModalVisible(true); fetchFunctionDetails(); } @@ -307,9 +277,8 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF // Handler to open the README analysis modal const handleReadmeAnalysisClick = () => { - if (hasReadMeAnalysis && !isReadMeAnalyzing) { + if (hasReadMeAnalysis && !isDocQualityAnalyzing) { setIsReadmeModalVisible(true); - fetchReadMeDetails(); } }; @@ -328,76 +297,61 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF }; // Add a handler function for status updates from the terminal - const handleStatusUpdate = (percentage: number, response?: AnalysisReport ) => { + const handleStatusUpdate = (percentage: number, response?: AnalysisReport) => { setProgress(percentage); - if (response) { - // Create a clone of the current repoDetails to avoid direct mutation - const updatedRepoDetails = { ...repoDetails }; - - // Update the analysis report with the new data - const newAnalysisReport = response; - - if(!updatedRepoDetails.gitAppMetaData) { - updatedRepoDetails.gitAppMetaData = { - siteUrl: '', - analysisReport: {} as AnalysisReport - }; - } - updatedRepoDetails.gitAppMetaData.analysisReport = newAnalysisReport; - - // Update the local metrics based on the new report - updateMetricsFromReport(newAnalysisReport); - - // Update progress state - - - - // Set analysis flags based on the updated report - if (newAnalysisReport.basicFuncDocAnalysis?.totalFunctions > 0) { - setHasRepoAnalysis(true); - setIsRepoAnalyzing(false); - } - - if (newAnalysisReport.advancedFuncDocAnalysis) { - setHasAdvancedRepoAnalysis(true); - setIsAdvancedRepoAnalyzing(false); - } - - if (newAnalysisReport.readmeAnalysis && newAnalysisReport.readmeAnalysis.calculatedQualityScore >= 0) { - setHasReadmeAnalysis(true); - setIsReadMeAnalyzing(false); + if (percentage >= 100 && !completionHandledRef.current) { + completionHandledRef.current = true; + setIsDocQualityAnalyzing(false); + + if (!response) { + void (async () => { + try { + const gitData = await getGitApp(orgName, repoName, repoDetails.vendor); + const latestReport = gitData?.gitAppAdvancedMeta?.analysisReport; + if (latestReport) { + updateMetricsFromReport(latestReport); + setHasRepoAnalysis(!!latestReport.basicFuncDocAnalysis?.totalFunctions); + setHasAdvancedRepoAnalysis(!!latestReport.advancedFuncDocAnalysis); + setHasReadmeAnalysis( + !!latestReport.readmeAnalysis && + latestReport.readmeAnalysis.calculatedQualityScore >= 0, + ); + } + } catch (error) { + console.error('Error fetching analysis report:', error); + } + })(); } - + } + + if (response) { + updateMetricsFromReport(response); + setHasRepoAnalysis(!!response.basicFuncDocAnalysis?.totalFunctions); + setHasAdvancedRepoAnalysis(!!response.advancedFuncDocAnalysis); + setHasReadmeAnalysis(!!response.readmeAnalysis && response.readmeAnalysis.calculatedQualityScore >= 0); + if (percentage >= 100) setIsDocQualityAnalyzing(false); } }; // Handle PAT modal submission success const handlePATSubmitSuccess = () => { setAzurePATModalVisible(false); - - // Resume the pending analysis if there is one - if (pendingAnalysis) { - runAnalysis( - pendingAnalysis.setIsAnalyzing, - pendingAnalysis.hasReportAnalysis, - pendingAnalysis.analysisType - ); - - // Clear the pending analysis - setPendingAnalysis(null); + if (resumeAfterPat) { + setResumeAfterPat(false); + void runDocQualityAnalysis(); } }; // Handle PAT modal cancellation const handlePATModalCancel = () => { setAzurePATModalVisible(false); - - // If there was a pending analysis, mark it as not analyzing anymore - if (pendingAnalysis) { - pendingAnalysis.setIsAnalyzing(false); - setPendingAnalysis(null); - - addTerminalMessage(`Analysis cancelled: Azure DevOps credentials required`, MessageType.ERROR); + if (resumeAfterPat) { + setResumeAfterPat(false); + setIsDocQualityAnalyzing(false); + addTerminalMessage( + `Analysis cancelled: Azure DevOps credentials required`, + MessageType.ERROR, + ); } }; @@ -407,42 +361,19 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF - {hasRepoAnalysis && !isRepoAnalyzing && ( - } - onClick={(e) => { - e.stopPropagation(); - runAnalysis(setIsRepoAnalyzing, setHasRepoAnalysis, "basicDoc"); - }} - /> - )} - - {!hasRepoAnalysis && !isRepoAnalyzing ? ( + {isDocQualityAnalyzing ? ( + + + Analyzing doc quality: {progress}% + + ) : !hasRepoAnalysis ? ( - No Repository Analysis Data - - - ) : isRepoAnalyzing ? ( - - - Analyzing files: {progress}% + Run Doc Quality analysis to see metrics ) : ( <> @@ -461,37 +392,17 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF - {hasRepoAnalysis && !isRepoAnalyzing && ( - } - onClick={(e) => { - e.stopPropagation(); - runAnalysis(setIsRepoAnalyzing, setHasRepoAnalysis, undefined); - }} - /> - )} - - {(!hasRepoAnalysis && !isRepoAnalyzing) || Object.keys(programmingLanguages).length === 0 ? ( + {isDocQualityAnalyzing ? ( + + + Analyzing doc quality: {progress}% + + ) : Object.keys(programmingLanguages).length === 0 ? ( - Languages not analyzed - - - ) : isRepoAnalyzing ? ( - - - Analyzing repository: {progress}% + Run Doc Quality analysis to detect languages ) : ( <> @@ -541,39 +452,19 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF - {hasAdvancedRepoAnalysis && !isAdvancedRepoAnalyzing && ( - } - onClick={(e) => { - e.stopPropagation(); - runAnalysis(setIsAdvancedRepoAnalyzing, setHasAdvancedRepoAnalysis, "advancedDoc"); - }} - /> - )} - - {!hasAdvancedRepoAnalysis && !isAdvancedRepoAnalyzing ? ( + {isDocQualityAnalyzing ? ( + + + Analyzing doc quality: {progress}% + + ) : !hasAdvancedRepoAnalysis ? ( - No documentation issues data - - - ) : isAdvancedRepoAnalyzing ? ( - - - Finding documentation issues: {progress}% + Run Doc Quality analysis to detect issues ) : ( <> @@ -582,7 +473,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF
- {outdatedDocs}/{documentedFunctions} + {outdatedDocs+(totalFunctions-documentedFunctions)}/{totalFunctions} Outdated Docs
@@ -594,10 +485,10 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF
Outdated - {parseFloat(((outdatedDocs / totalFunctions) * 100).toFixed(1))}% + {parseFloat((((outdatedDocs+(totalFunctions-documentedFunctions)) / totalFunctions) * 100).toFixed(1))}%
= ({ repoDetails, onRunF - {hasReadMeAnalysis && !isReadMeAnalyzing && ( - } - onClick={(e) => { - e.stopPropagation(); - runAnalysis(setIsReadMeAnalyzing, setHasReadmeAnalysis, "readme"); - }} - /> - )} - - {!hasReadMeAnalysis && !isReadMeAnalyzing ? ( + {isDocQualityAnalyzing ? ( + + + Analyzing doc quality: {progress}% + + ) : !hasReadMeAnalysis ? ( - README not analyzed - - - ) : isReadMeAnalyzing ? ( - - - Analyzing README: {progress}% + Run Doc Quality analysis to score README ) : ( <> - {readmeScore.toFixed(1)}%/100 + {readmeScore?.toFixed(1)|| 0}% Readme Quality Score 70 ? '#52c41a' : readmeScore > 40 ? '#faad14' : '#f5222d'} /> @@ -678,9 +546,9 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF - {isRepoAnalyzing && ( + {isDocQualityAnalyzing && ( - Analyzing repository documentation... Please wait + Analyzing documentation quality... Please wait )} @@ -706,10 +574,9 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF setIsReadmeModalVisible(false)} - readmeAnalysis={repoDetails?.gitAppMetaData?.analysisReport?.readmeAnalysis} + readmeAnalysis={repoDetails?.gitAppAdvancedMeta?.analysisReport?.readmeAnalysis} repoName={repoDetails?.repoName} orgName={repoDetails?.organizationName} - readmeSuggestion={readMeDetails} /> {/* Replace Modal with direct CustomTerminal */} @@ -728,7 +595,7 @@ const RepoMetricsCards: React.FC = ({ repoDetails, onRunF orgName={orgName} repoName={repoName} vendor={repoDetails.vendor} - statusCheckFn={getAnalysisRepoStatus} + statusCheckFn={getDocGenStatus} onStatusUpdate={handleStatusUpdate} // Add the new status update handler /> diff --git a/src/components/dashboard/DashboardList/ListAllRepos/ConnectMoreVendors.tsx b/src/components/dashboard/DashboardList/ListAllRepos/ConnectMoreVendors.tsx index 23875250..a29b0aaa 100644 --- a/src/components/dashboard/DashboardList/ListAllRepos/ConnectMoreVendors.tsx +++ b/src/components/dashboard/DashboardList/ListAllRepos/ConnectMoreVendors.tsx @@ -250,7 +250,7 @@ const ConnectMoreVendors: React.FC = ({ existingVendors return ( <> - {/* = ({ existingVendors > Refresh Vendors - */} + {/* Terminal Modal - moved outside popover with proper modal styling */} {isModalVisible && ( diff --git a/src/components/dashboard/DashboardList/ListAllRepos/ListAllRepos.tsx b/src/components/dashboard/DashboardList/ListAllRepos/ListAllRepos.tsx index 486da22c..3d7ad83c 100644 --- a/src/components/dashboard/DashboardList/ListAllRepos/ListAllRepos.tsx +++ b/src/components/dashboard/DashboardList/ListAllRepos/ListAllRepos.tsx @@ -7,9 +7,9 @@ import { VendorDropdown, VendorIconKey, vendorIcon } from '../../common/VendorDr import { InnerBaseCard, OuterBaseCard, TransparentCard } from './ListAllRepos.Styles'; import { BaseRow } from '@app/components/common/BaseRow/BaseRow'; import { BaseCol } from '@app/components/common/BaseCol/BaseCol'; -import { EyeOutlined, GithubOutlined, GitlabOutlined, CloudOutlined } from '@ant-design/icons'; +import { EyeOutlined, GithubOutlined, GitlabOutlined } from '@ant-design/icons'; import { SearchDropdown } from '../../common/SearchDropdown/SearchDropdown'; -import { Button, Typography, Modal, Form, Input, Select } from 'antd'; +import { Button, Typography, Modal } from 'antd'; import ConnectMoreVendors from './ConnectMoreVendors'; import { v4 as uuidv4 } from 'uuid'; import CustomTerminal, { MessageType, TerminalMessage } from '../../DashboardTerminal/CustomTerminal'; @@ -18,6 +18,7 @@ import { readToken } from '@app/services/localStorage.service'; import { BitBucketOutlined } from '../../common/VendorDropdown/vendorIcons/VendorIcons'; import './IntegrationButtons.css'; import { AzureOutlined } from '../../common/VendorDropdown/vendorIcons/VendorIcons'; +import { decodeGitOrgName } from '@app/utils/gitNameEncoding'; interface GroupedReposType { [key: string]: GitAppUsageType[]; @@ -30,7 +31,6 @@ const EmptyReposMessage: React.FC = () => { const [isInstallationComplete, setIsInstallationComplete] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [installationType, setInstallationType] = useState<'bitbucket' | 'gitlab' | 'azure'>('bitbucket'); - const [form] = Form.useForm(); let eventSourceWithAuth: EventSourceWithAuth | null = null; // Function to handle BitBucket App installation @@ -320,28 +320,58 @@ export const ListAllRepos: React.FC = () => { const [selectedVendor, setSelectedVendor] = useState('all'); const [vendorOptions, setVendorOptions] = useState([]); - const fetchRepos = useCallback(() => { - getGitAppUsage() - .then((resp) => { - setRepos(resp); + const REPO_CACHE_KEY = 'penify.repoListCache.v1'; - const uniqueVendors = Array.from(new Set(resp.map((item) => item.vendor))); - setVendorOptions(['All', ...uniqueVendors]); - - // If there's only one vendor, automatically select it - if (uniqueVendors.length === 1) { - setSelectedVendor(uniqueVendors[0].toLowerCase()); - } - }) - .catch((err) => { - console.error(err); - }) - .finally(() => setIsLoading(false)); + const applyRepoList = useCallback((resp: GitAppUsageType[]) => { + setRepos(resp); + + const uniqueVendors = Array.from(new Set(resp.map((item) => item.vendor))); + setVendorOptions(['All', ...uniqueVendors]); + + if (uniqueVendors.length === 1) { + setSelectedVendor(uniqueVendors[0].toLowerCase()); + } }, []); + const loadCachedRepos = useCallback((): boolean => { + const cached = localStorage.getItem(REPO_CACHE_KEY); + if (!cached) return false; + + try { + const parsed = JSON.parse(cached) as { repos: GitAppUsageType[]; updatedAt: number }; + if (Array.isArray(parsed.repos)) { + applyRepoList(parsed.repos); + setIsLoading(false); + return true; + } + } catch { + // Ignore cache parsing errors + } + + return false; + }, [applyRepoList]); + + const fetchRepos = useCallback( + async ({ showLoader }: { showLoader: boolean }) => { + if (showLoader) setIsLoading(true); + + try { + const resp = await getGitAppUsage(); + applyRepoList(resp); + localStorage.setItem(REPO_CACHE_KEY, JSON.stringify({ repos: resp, updatedAt: Date.now() })); + } catch (err) { + console.error(err); + } finally { + setIsLoading(false); + } + }, + [applyRepoList], + ); + useEffect(() => { - fetchRepos(); - }, [fetchRepos]); + const hadCache = loadCachedRepos(); + fetchRepos({ showLoader: !hadCache }); + }, [fetchRepos, loadCachedRepos]); const filteredRepos = useMemo(() => { if (selectedVendor === 'all') { @@ -400,7 +430,7 @@ export const ListAllRepos: React.FC = () => { v !== 'All')} - onVendorConnected={fetchRepos} + onVendorConnected={() => fetchRepos({ showLoader: true })} /> {vendorOptions.filter(v => v !== 'All').length > 1 && ( { .sort(([a], [b]) => a.localeCompare(b)) .map(([organization, repos]) => ( - + {repos .sort((a, b) => { diff --git a/src/components/dashboard/DashboardList/ListOrgs/ListOrgs.tsx b/src/components/dashboard/DashboardList/ListOrgs/ListOrgs.tsx index 47d91b31..aa786476 100644 --- a/src/components/dashboard/DashboardList/ListOrgs/ListOrgs.tsx +++ b/src/components/dashboard/DashboardList/ListOrgs/ListOrgs.tsx @@ -9,6 +9,7 @@ import { BaseCol } from '@app/components/common/BaseCol/BaseCol'; import { EyeOutlined } from '@ant-design/icons'; import { InnerBaseCard, OuterBaseCard, TransparentCard } from '../ListAllRepos/ListAllRepos.Styles'; import { SearchDropdown } from '../../common/SearchDropdown/SearchDropdown'; +import { decodeGitOrgName } from '@app/utils/gitNameEncoding'; export const ListOrgs: React.FC = () => { const navigate = useNavigate(); @@ -75,7 +76,7 @@ export const ListOrgs: React.FC = () => { - + {filteredRepos .sort((a, b) => { diff --git a/src/components/dashboard/DashboardTerminal/ArchConfigModal.tsx b/src/components/dashboard/DashboardTerminal/ArchConfigModal.tsx index f27823fe..c5896fcd 100644 --- a/src/components/dashboard/DashboardTerminal/ArchConfigModal.tsx +++ b/src/components/dashboard/DashboardTerminal/ArchConfigModal.tsx @@ -95,6 +95,7 @@ interface ArchConfigModalProps { onCancel: () => void; onConfirm: (archConfig: ArchDocConfig) => void; loading?: boolean; + isPrivate?: boolean; } /** @@ -112,6 +113,7 @@ const ArchConfigModal: React.FC = ({ onCancel, onConfirm, loading = false, + isPrivate = false, }) => { const [form] = Form.useForm(); @@ -119,7 +121,8 @@ const ArchConfigModal: React.FC = ({ // Set default values when modal becomes visible if (visible) { form.setFieldsValue({ - hostDocumentation: true, + // For private repositories, hostDocumentation should be false by default + hostDocumentation: !isPrivate, generatePdf: true, generateMd: true, generateHtml: true, @@ -130,12 +133,23 @@ const ArchConfigModal: React.FC = ({ path: 'docs' }); } - }, [visible, form]); + }, [visible, form, isPrivate]); /** * Handle form submission and pass the config to the parent component */ - const handleFinish = (values: any) => { + const handleFinish = (values: { + hostDocumentation: boolean; + generatePdf: boolean; + generateMd: boolean; + generateHtml: boolean; + generateRst: boolean; + generateRtf: boolean; + generateXml: boolean; + commitToGit: boolean; + branch: string; + path: string; + }) => { const { commitToGit, branch, path, ...rest } = values; const archConfig: ArchDocConfig = { @@ -306,8 +320,8 @@ const ArchConfigModal: React.FC = ({ {({ getFieldValue, setFieldsValue }) => { const generateHtml = getFieldValue('generateHtml'); - // If HTML is unchecked, also uncheck the host documentation option - if (!generateHtml && getFieldValue('hostDocumentation')) { + // If HTML is unchecked or it's a private repository, also uncheck the host documentation option + if ((!generateHtml || isPrivate) && getFieldValue('hostDocumentation')) { setFieldsValue({ hostDocumentation: false }); } @@ -316,13 +330,17 @@ const ArchConfigModal: React.FC = ({ name="hostDocumentation" valuePropName="checked" > - - + + Host Documentation Online - - + "HTML format must be selected to enable online hosting" + }> + diff --git a/src/components/dashboard/DashboardTerminal/CustomTerminal.tsx b/src/components/dashboard/DashboardTerminal/CustomTerminal.tsx index 3e7ff8c4..26970609 100644 --- a/src/components/dashboard/DashboardTerminal/CustomTerminal.tsx +++ b/src/components/dashboard/DashboardTerminal/CustomTerminal.tsx @@ -326,6 +326,8 @@ const CustomTerminal: React.FC = ({ // Use a ref to store the latest messages to avoid closure issues const messagesRef = useRef(messages); const jobIdRef = useRef(jobId); + const onStatusUpdateRef = useRef(onStatusUpdate); + const statusCheckFnRef = useRef(statusCheckFn); // Update the ref when messages change useEffect(() => { @@ -333,97 +335,104 @@ const CustomTerminal: React.FC = ({ setLocalMessages(messages); }, [messages]); + useEffect(() => { + onStatusUpdateRef.current = onStatusUpdate; + }, [onStatusUpdate]); + + useEffect(() => { + statusCheckFnRef.current = statusCheckFn; + }, [statusCheckFn]); + // Status polling logic - let countOfFailedStatus = 0; + const failedStatusCountRef = useRef(0); useEffect(() => { jobIdRef.current = jobId; - if(messages.length > 0) { - if( messages[messages.length - 1] && messages[messages.length - 1].type) { - const messageType = messages[messages.length - 1].type; - if ([MessageType.COMPLETE, MessageType.TERMINATED, MessageType.FAILED, MessageType.TIMEOUT].includes(messageType)) { + failedStatusCountRef.current = 0; + + if (!jobId || !statusCheckFnRef.current) { + setLocalIsProcessing(false); + return; + } + + setLocalIsProcessing(true); + const intervalId = setInterval(async () => { + const latestMessages = messagesRef.current; + if (latestMessages.length > 0) { + const lastType = latestMessages[latestMessages.length - 1]?.type; + if ( + lastType && + [MessageType.COMPLETE, MessageType.TERMINATED, MessageType.FAILED, MessageType.TIMEOUT].includes(lastType) + ) { setLocalIsProcessing(false); + clearInterval(intervalId); return; } } - } - if (jobId && statusCheckFn && messages) { - setLocalIsProcessing(true); - const intervalId = setInterval(async () => { - try { - const response: DocGenStatusReponse = await statusCheckFn( - orgName, - repoName, - vendor, - terminalKey, - jobId - ); - - // Update percentage if provided - setLocalPercentage(response.percentage || 0); - if (onStatusUpdate && response.percentage) { - onStatusUpdate(response.percentage, undefined); - } - - // Process the messages - if (response.messages && Array.isArray(response.messages)) { - for (const message of response.messages) { - - switch (message.trim()) { - case 'CANCELLED': - addMessage('Complete!', MessageType.TERMINATED); - break; - case 'COMPLETE': - addMessage('Complete!', MessageType.COMPLETE); - break; - case 'TIMEOUT': - addMessage('Timeout', MessageType.TIMEOUT); - break; - case 'TERMINATED': - addMessage('Cancelled', MessageType.TERMINATED); - break; - case 'FAILED': - addMessage('Failed', MessageType.FAILED); - break; - default: - parseAndAddMessage(message); - continue; - } - setTimeout(async () => { - setLocalIsProcessing(false); - const gitData = await getGitApp(orgName, repoName, vendor); - - // Propagate status update to parent component - if (onStatusUpdate && gitData) { - if(gitData.gitAppMetaData && gitData.gitAppMetaData.analysisReport) { - onStatusUpdate(100, gitData.gitAppMetaData.analysisReport); - } else { - onStatusUpdate(100, undefined); - } - - } - - clearInterval(intervalId); - }, 100); + + try { + const response: DocGenStatusReponse = await statusCheckFnRef.current( + orgName, + repoName, + vendor, + terminalKey, + jobId, + ); + + const nextPercentage = response.percentage || 0; + setLocalPercentage(nextPercentage); + onStatusUpdateRef.current?.(nextPercentage, undefined); + + if (response.messages && Array.isArray(response.messages)) { + for (const message of response.messages) { + const trimmed = message.trim(); + const isTerminalState = ['CANCELLED', 'COMPLETE', 'TIMEOUT', 'TERMINATED', 'FAILED'].includes(trimmed); + + if (!isTerminalState) { + parseAndAddMessage(message); + continue; } - } - } catch (error) { - console.error('Error checking status:', error); - addMessage(`Error checking status: ${error}`, MessageType.ERROR); - countOfFailedStatus++; - if (countOfFailedStatus >= 5) { - addMessage('Failed to check status after multiple attempts.', MessageType.FAILED); + + const type = + trimmed === 'COMPLETE' + ? MessageType.COMPLETE + : trimmed === 'FAILED' + ? MessageType.FAILED + : trimmed === 'TIMEOUT' + ? MessageType.TIMEOUT + : MessageType.TERMINATED; + + addMessage(trimmed === 'CANCELLED' ? 'Cancelled' : trimmed, type); + setLocalIsProcessing(false); clearInterval(intervalId); + + try { + const gitData = await getGitApp(orgName, repoName, vendor); + onStatusUpdateRef.current?.(100, gitData?.gitAppAdvancedMeta?.analysisReport, type); + } catch (err) { + console.error('Error fetching repo data after completion:', err); + onStatusUpdateRef.current?.(100, undefined, type); + } + return; } } - }, 3000); // Poll every 3 seconds - - return () => { - - }; - } - }, [jobId, orgName, repoName]); + } catch (error) { + console.error('Error checking status:', error); + addMessage(`Error checking status: ${error}`, MessageType.ERROR); + failedStatusCountRef.current += 1; + if (failedStatusCountRef.current >= 5) { + addMessage('Failed to check status after multiple attempts.', MessageType.FAILED); + setLocalIsProcessing(false); + clearInterval(intervalId); + } + } + }, 3000); + + return () => { + clearInterval(intervalId); + }; + }, [jobId, orgName, repoName, vendor, terminalKey]); // Function to format JSON with syntax highlighting const formatJson = (jsonString: string): string => { @@ -440,6 +449,19 @@ const CustomTerminal: React.FC = ({ } }; + const parseTimestamp = (timestamp: string): Date => { + // Attempt to parse the timestamp string directly + const date = new Date(timestamp); + + // If the date is invalid, return current date + if (isNaN(date.getTime())) { + console.error('Invalid timestamp format:', timestamp); + return new Date(); + } + + return date; + } + // Parse messages from the status response const parseAndAddMessage = (message: string) => { // Skip empty messages @@ -451,62 +473,59 @@ const CustomTerminal: React.FC = ({ // If we have at least 3 parts, we have a properly formatted message if (parts.length >= 3) { const messageType = parts[0]; - let timestamp = new Date(); - let content = parts.slice(2).join('::'); // Join the rest back in case message itself contains :: - - // Try to parse the timestamp - try { - timestamp = new Date(parts[1]); - } catch (error) { - console.error('Error parsing timestamp:', error); - } - - // Determine message type - let type = MessageType.INFO; + const timestamp = parseTimestamp(parts[1]); // Parse the timestamp from the second part + const contentList = parts.slice(2).join('::').split('\n'); + + for (let content of contentList) { + if( !content || content.trim() === '') continue; // Skip empty lines - switch (messageType) { - case 'ERROR': - type = MessageType.ERROR; - break; - case 'WARNING': - type = MessageType.WARNING; - break; - case 'INFO': - type = MessageType.INFO; - break; - case 'SUCCESS': - type = MessageType.SUCCESS; - break; - case 'DEBUG': - type = MessageType.DEBUG; - break; - case 'COMMAND': - type = MessageType.COMMAND; - break; - case 'JSON': - try { - // Parse JSON to validate and then stringify for formatting - const jsonContent = JSON.parse(content); - // Instead of just adding
 tags, create a message with special JSON formatting
-            addJsonMessage(jsonContent);
-            return; // Return early as we're handling this message separately
-          } catch (error) {
-            console.error('Error parsing JSON:', error);
-            content = `
${content}
`; + // Determine message type + let type = MessageType.INFO; + + switch (messageType) { + case 'ERROR': + type = MessageType.ERROR; + break; + case 'WARNING': + type = MessageType.WARNING; + break; + case 'INFO': type = MessageType.INFO; - } - break; - case 'URL': - // Make URLs clickable with HTML links - const urlRegex = /(https?:\/\/[^\s]+)/g; - content = content.replace(urlRegex, (url) => `${url}`); - type = MessageType.URL; - break; - default: - type = MessageType.INFO; + break; + case 'SUCCESS': + type = MessageType.SUCCESS; + break; + case 'DEBUG': + type = MessageType.DEBUG; + break; + case 'COMMAND': + type = MessageType.COMMAND; + break; + case 'JSON': + try { + // Parse JSON to validate and then stringify for formatting + const jsonContent = JSON.parse(content); + // Instead of just adding
 tags, create a message with special JSON formatting
+              addJsonMessage(jsonContent);
+              return; // Return early as we're handling this message separately
+            } catch (error) {
+              console.error('Error parsing JSON:', error);
+              content = `
${content}
`; + type = MessageType.INFO; + } + break; + case 'URL': + // Make URLs clickable with HTML links + const urlRegex = /(https?:\/\/[^\s]+)/g; + content = content.replace(urlRegex, (url) => `${url}`); + type = MessageType.URL; + break; + default: + type = MessageType.INFO; + } + + addMessage(content, type, timestamp); } - - addMessage(content, type, timestamp); } else { // If the message doesn't match the expected format, add it as-is diff --git a/src/components/dashboard/DashboardTerminal/DashboardTerminal.tsx b/src/components/dashboard/DashboardTerminal/DashboardTerminal.tsx index af226f25..0387ffa9 100644 --- a/src/components/dashboard/DashboardTerminal/DashboardTerminal.tsx +++ b/src/components/dashboard/DashboardTerminal/DashboardTerminal.tsx @@ -1,5 +1,5 @@ -import React, { useState, useRef } from 'react'; -import { Row, Col, Typography, Space, Divider, Button } from 'antd'; +import React, { useMemo, useRef } from 'react'; +import { Divider, Button, Typography } from 'antd'; import styled from 'styled-components'; import { BaseRow } from '@app/components/common/BaseRow/BaseRow'; import { BaseCol } from '@app/components/common/BaseCol/BaseCol'; @@ -8,19 +8,8 @@ import { useAppSelector } from '@app/hooks/reduxHooks'; import DocumentationTools from './DocumentationTools'; import RepoMetricsCards from '../DashboardHeader/RepoMetricsCards'; import { ScanOutlined } from '@ant-design/icons'; -import { useTranslation } from 'react-i18next'; import DocumentationInsights from './DocumentationInsights'; -const { Title } = Typography; - -const PageHeader = styled.div` - margin-bottom: 24px; -`; - -const SectionTitle = styled(Title)` - margin-bottom: 16px !important; -`; - const DashboardContainer = styled.div` margin-bottom: 40px; `; @@ -58,12 +47,10 @@ interface DashboardTerminalProps { export const DashboardTerminal: React.FC = ({ repoDetails }) => { const user = useAppSelector((state) => state.user.user); - const { t } = useTranslation(); - let hasAnalysisReport = false; - if (repoDetails?.gitAppMetaData?.analysisReport) { - hasAnalysisReport = true; - } - const [hasRunAnalysis, setHasRunAnalysis] = useState(true); + const hasAnalysisReport = useMemo( + () => !!repoDetails?.gitAppAdvancedMeta?.analysisReport, + [repoDetails], + ); // Create a ref to store the runFullAnalysis function from RepoMetricsCards const repoMetricsRef = useRef<(() => void) | null>(null); @@ -78,34 +65,30 @@ export const DashboardTerminal: React.FC = ({ repoDetail if (repoMetricsRef.current) { repoMetricsRef.current(); // This will call runFullDocumentationAnalysis in RepoMetricsCards } - setHasRunAnalysis(true); }; if (!user) return null; - // Get language data from repoDetails if available - const languageData = repoDetails?.gitAppMetaData?.analysisReport?.programmingLanguages || {}; - return ( - {!hasRunAnalysis && ( - - - Repository Documentation Analysis - -
- Run an analysis to see detailed documentation metrics for your repository -
- -
- )} + + + Doc Quality Analysis + +
+ Coverage, outdated docs, README quality, and language breakdown. +
+ +
{/* Repository Metrics Overview */} diff --git a/src/components/dashboard/DashboardTerminal/DocstringStyleModal.tsx b/src/components/dashboard/DashboardTerminal/DocstringStyleModal.tsx index 3e9d0a99..a819e42f 100644 --- a/src/components/dashboard/DashboardTerminal/DocstringStyleModal.tsx +++ b/src/components/dashboard/DashboardTerminal/DocstringStyleModal.tsx @@ -173,27 +173,15 @@ const DocstringStyleModal: React.FC = ({
Configure Docstring Styles - Personalize how your docstring is generated. - -
- - - e.stopPropagation()} - > - Learn more - - - } - description="Configure the docstring style and max line length." - type="info" - showIcon - /> + >Learn more. + +
+
= ({ onCancel={handleArchConfigModalCancel} onConfirm={handleArchConfigConfirm} loading={isLoading} + isPrivate={repoDetails.isPrivate} /> {/* Azure DevOps PAT Modal */} diff --git a/src/components/dashboard/DashboardTerminal/DocumentationInsights.tsx b/src/components/dashboard/DashboardTerminal/DocumentationInsights.tsx index a19d6724..bcc79799 100644 --- a/src/components/dashboard/DashboardTerminal/DocumentationInsights.tsx +++ b/src/components/dashboard/DashboardTerminal/DocumentationInsights.tsx @@ -11,7 +11,6 @@ import { } from '@ant-design/icons'; import styled from 'styled-components'; import { GitAppUsageType } from '@app/api/git.api'; -import { c } from 'node_modules/vite/dist/node/types.d-aGj9QkWt'; const { Text, Title } = Typography; @@ -93,7 +92,7 @@ const DocumentationInsights: React.FC = ({ repoDetai const [hasData, setHasData] = useState(false); // Extract data from repoDetails if available - const analysisReport = repoDetails?.gitAppMetaData?.analysisReport; + const analysisReport = repoDetails?.gitAppAdvancedMeta?.analysisReport; const basicFuncDocAnalysis = analysisReport?.basicFuncDocAnalysis; const advancedFuncDocAnalysis = analysisReport?.advancedFuncDocAnalysis; const readmeAnalysis = analysisReport?.readmeAnalysis; @@ -193,7 +192,7 @@ const DocumentationInsights: React.FC = ({ repoDetai icon: , iconColor: '#faad14', title: 'README needs improvement', - description: `Current quality score: ${readmeAnalysis.calculatedQualityScore.toFixed(1)}%`, + description: `Current quality score: ${readmeAnalysis?.calculatedQualityScore?.toFixed(1)}%`, priority: readmeAnalysis.calculatedQualityScore < 30 ? 'high' : 'medium' }); } else if (readmeAnalysis.calculatedQualityScore >= 80) { @@ -201,7 +200,7 @@ const DocumentationInsights: React.FC = ({ repoDetai icon: , iconColor: '#52c41a', title: 'README quality is excellent', - description: `Quality score: ${readmeAnalysis.calculatedQualityScore.toFixed(1)}%`, + description: `Quality score: ${readmeAnalysis?.calculatedQualityScore?.toFixed(1)}%`, priority: 'low' }); } diff --git a/src/components/dashboard/DashboardTerminal/DocumentationTools.tsx b/src/components/dashboard/DashboardTerminal/DocumentationTools.tsx index ab79c83d..ce2f8d16 100644 --- a/src/components/dashboard/DashboardTerminal/DocumentationTools.tsx +++ b/src/components/dashboard/DashboardTerminal/DocumentationTools.tsx @@ -37,6 +37,7 @@ const StyledTabs = styled(Tabs)` .ant-tabs-tab-btn { font-weight: 500; + font-size: 13px; display: flex; align-items: center; gap: 8px; @@ -100,7 +101,8 @@ const CardTitle = styled.div` h4 { margin: 0; - font-weight: 600; + font-weight: 500; + font-size: 16px; } .badge-container { @@ -119,8 +121,8 @@ const TabDescription = styled(Paragraph)` border-left: 4px solid #1890ff; .title { - font-weight: 600; - font-size: 16px; + font-weight: 500; + font-size: 15px; margin-bottom: 8px; color: #111; } @@ -128,14 +130,16 @@ const TabDescription = styled(Paragraph)` .description { margin: 0; color: rgba(0, 0, 0, 0.65); + font-size: 13px; } `; const StyledTag = styled(Tag)` margin-left: 8px; - font-weight: 500; + font-weight: 400; padding: 2px 8px; border-radius: 4px; + font-size: 12px; `; const ValueProposition = styled.div` @@ -154,6 +158,7 @@ const ValuePoint = styled.div` color: #52c41a; margin-right: 10px; margin-top: 2px; + font-size: 14px; } .content { @@ -163,14 +168,18 @@ const ValuePoint = styled.div` .title { font-weight: 500; margin-bottom: 2px; + font-size: 14px; } .description { color: rgba(0, 0, 0, 0.65); - font-size: 13px; + font-size: 12px; } `; +// These styled components might be used in the future +// Temporarily commented out to avoid unused component warnings +/* const BeforeAfterPanel = styled.div` margin: 20px 0; `; @@ -223,6 +232,7 @@ const GradientBadge = styled.div` margin-right: 4px; } `; +*/ interface DocumentationToolsProps { repoDetails: GitAppUsageType; @@ -230,7 +240,8 @@ interface DocumentationToolsProps { const DocumentationTools: React.FC = ({ repoDetails }) => { const user = useAppSelector((state) => state.user.user); - const [activeTab, setActiveTab] = useState('code'); + // Track active tab for future use + const [, setActiveTab] = useState('code'); if (!user) return null; @@ -278,7 +289,7 @@ function calculateMetrics(data) { const tutorialCardForDocGen = ( <>
- + How it looks like? - + <Title level={5} style={{ marginTop: 0, marginBottom: 16, fontSize: '15px', fontWeight: 500 }}> Why document your code? @@ -333,7 +344,7 @@ function calculateMetrics(data) { - Repository Documentation Tools + Repository Documentation Tools {/*
{user.countRepoGen > 0 && ( @@ -365,7 +376,7 @@ function calculateMetrics(data) { key="code" >
- +
Create comprehensive docstrings for functions and classes throughout your codebase. @@ -397,7 +408,7 @@ function calculateMetrics(data) { key="architecture" >
- +
Generate Architecture Documentation
Create high-level architecture documentation that explains @@ -423,7 +434,7 @@ function calculateMetrics(data) { key="api" >
- +
Generate API Documentation
Generate detailed API documentation for endpoints, request/response formats, and diff --git a/src/components/dashboard/common/SearchDropdown/SearchDropdown.tsx b/src/components/dashboard/common/SearchDropdown/SearchDropdown.tsx index 33c992e9..7c5e2ef0 100644 --- a/src/components/dashboard/common/SearchDropdown/SearchDropdown.tsx +++ b/src/components/dashboard/common/SearchDropdown/SearchDropdown.tsx @@ -5,6 +5,7 @@ import { BaseList } from '@app/components/common/BaseList/BaseList'; import { HashLink } from 'react-router-hash-link'; import { NotFound } from '@app/components/common/NotFound/NotFound'; import { VendorIconKey, vendorIcon } from '../VendorDropdown/VendorDropdown'; +import { decodeGitOrgName } from '@app/utils/gitNameEncoding'; type SearchDropdownProps = { repos: GitAppUsageType[]; @@ -42,7 +43,7 @@ export const SearchDropdown: React.FC = ({ repos }) => { avatar={vendorIcon[item.vendor.toLowerCase() as VendorIconKey]} description={ - {item.organizationName} / {item.repoName} + {decodeGitOrgName(item.organizationName)} / {item.repoName} } /> diff --git a/src/components/jobs/AllJobsList/AllJobsList.tsx b/src/components/jobs/AllJobsList/AllJobsList.tsx index e702d551..15d80fe9 100644 --- a/src/components/jobs/AllJobsList/AllJobsList.tsx +++ b/src/components/jobs/AllJobsList/AllJobsList.tsx @@ -158,10 +158,14 @@ export const AllJobsList: React.FC = () => { try { const { logs } = await getLroJobLogs(job.id || -1); - for (const log of logs.split('\n')) { + // this is the seperator we use to split logs in backend + // '\n|||' is used to separate log messages in the backend + // it is used to split logs into individual messages + for (const log of logs.split('\n|||')) { if (!log.trim()) continue; const [logType, ts, msg] = log.split('::'); + if(!msg ) continue; const message: TerminalMessage = { content: msg, type: MessageType[logType.toUpperCase() as keyof typeof MessageType] || MessageType.INFO, diff --git a/src/components/layouts/main/MainNav/MainNavs.tsx b/src/components/layouts/main/MainNav/MainNavs.tsx index 0ba9f5bc..b3a9eb68 100644 --- a/src/components/layouts/main/MainNav/MainNavs.tsx +++ b/src/components/layouts/main/MainNav/MainNavs.tsx @@ -8,6 +8,7 @@ import { Link, useLocation, useNavigate, useParams, useSearchParams } from 'reac import { MainBreadcrumb, MainBreadcrumbWrapper, RepoActionsContainer } from './MainNavs.Styles'; import { useTranslation } from 'react-i18next'; import { AzureOutlined, BitBucketOutlined } from '@app/components/dashboard/common/VendorDropdown/vendorIcons/VendorIcons'; +import { decodeGitOrgName } from '@app/utils/gitNameEncoding'; const pathToBreadcrumbMap: Record = { @@ -23,27 +24,28 @@ const pathToBreadcrumbMap: Record = { }; const getVendorDetails = (vendor: string, orgName: string, repoName: string) => { + const decodedOrgName = decodeGitOrgName(orgName); switch (vendor) { case 'BITBUCKET': return { - url: `https://bitbucket.org/${orgName}/${repoName}`, + url: `https://bitbucket.org/${decodedOrgName}/${repoName}`, icon: , }; case 'GITLAB': return { - url: `https://gitlab.com/${orgName}/${repoName}`, + url: `https://gitlab.com/${decodedOrgName}/${repoName}`, icon: , }; case 'AZUREDEVOPS': return { - url: `https://dev.azure.com/${orgName}/_git/${repoName}`, + url: `https://dev.azure.com/${decodedOrgName}/_git/${repoName}`, icon: , }; case 'GITHUB': default: return { - url: `https://github.com/${orgName}/${repoName}`, + url: `https://github.com/${decodedOrgName}/${repoName}`, icon: , }; } diff --git a/src/components/layouts/main/sider/SiderMenu/RepoNav.styles.ts b/src/components/layouts/main/sider/SiderMenu/RepoNav.styles.ts new file mode 100644 index 00000000..47ca5535 --- /dev/null +++ b/src/components/layouts/main/sider/SiderMenu/RepoNav.styles.ts @@ -0,0 +1,98 @@ +import styled, { css } from 'styled-components'; + +export const RepoSectionContainer = styled.div` + margin: 0 0 8px 0; + padding: 0; +`; + +export const RepoHeader = styled.div` + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: 6px; + margin: 0 10px 8px; + background-color: rgba(56, 189, 248, 0.1); + border-left: 3px solid #38BDF8; + overflow: hidden; +`; + +export const RepoIcon = styled.div` + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + background-color: #1f2937; + margin-right: 12px; + flex-shrink: 0; + + svg { + font-size: 16px; + color: #38BDF8; + } +`; + +export const RepoInfo = styled.div` + display: flex; + flex-direction: column; + min-width: 0; +`; + +export const RepoName = styled.div` + font-weight: 500; + font-size: 13px; + color: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const RepoOrg = styled.div` + font-size: 11px; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const RepoNavContainer = styled.div` + margin: 0 10px; +`; + +export const RepoNavItem = styled.div<{ isActive?: boolean }>` + display: flex; + align-items: center; + padding: 8px 12px; + color: #e2e8f0; + border-radius: 6px; + margin-bottom: 4px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 13px; + height: 36px; + line-height: 20px; + + ${props => props.isActive && css` + background-color: rgba(56, 189, 248, 0.15); + font-weight: 500; + color: white; + + svg { + color: #38BDF8; + } + + &:hover { + background-color: rgba(56, 189, 248, 0.2); + } + `} + + &:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + svg { + font-size: 16px; + margin-right: 8px; + } +`; diff --git a/src/components/layouts/main/sider/SiderMenu/RepoNav.tsx b/src/components/layouts/main/sider/SiderMenu/RepoNav.tsx new file mode 100644 index 00000000..e7051a3d --- /dev/null +++ b/src/components/layouts/main/sider/SiderMenu/RepoNav.tsx @@ -0,0 +1,90 @@ +import React, { useMemo } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + AreaChartOutlined, + FileTextOutlined, + SafetyOutlined, + GithubOutlined +} from '@ant-design/icons'; +import * as S from './RepoNav.styles'; +import { Badge, Tooltip } from 'antd'; + +interface RepoNavProps { + vendor: string; + orgName: string; + repoName: string; +} + +const RepoNav: React.FC = ({ vendor, orgName, repoName }) => { + const { t } = useTranslation(); + const location = useLocation(); + const currentPath = location.pathname; + + // Define navigation items + const navItems = useMemo(() => [ + { + key: 'analyze', + title: t('sidebar.analyze'), + icon: , + url: `/repositories/${vendor}/${orgName}/${repoName}/analyze`, + badge: null + }, + { + key: 'docufy', + title: t('sidebar.docufy'), + icon: , + url: `/repositories/${vendor}/${orgName}/${repoName}/docufy`, + badge: null + }, + { + key: 'security', + title: t('sidebar.security'), + icon: , + url: `/repositories/${vendor}/${orgName}/${repoName}/security`, + badge: null + } + ], [t, vendor, orgName, repoName]); + + return ( + + + + + + + {repoName} + {orgName} + + + + + {navItems.map(item => { + const isActive = currentPath === item.url; + + return ( + + + {item.icon} + {item.title} + {item.badge && ( + + + + )} + + + ); + })} + + + ); +}; + +export default RepoNav; diff --git a/src/components/layouts/main/sider/SiderMenu/SiderMenu.styles.ts b/src/components/layouts/main/sider/SiderMenu/SiderMenu.styles.ts index 6375c7d9..13301a94 100644 --- a/src/components/layouts/main/sider/SiderMenu/SiderMenu.styles.ts +++ b/src/components/layouts/main/sider/SiderMenu/SiderMenu.styles.ts @@ -4,7 +4,7 @@ import { BaseMenu } from '@app/components/common/BaseMenu/BaseMenu'; export const MainMenuWrapper = styled.div` flex: 1; overflow-y: auto; - padding-top: 12px; + padding-top: 8px; &::-webkit-scrollbar { width: 4px; @@ -42,12 +42,13 @@ export const Menu = styled(BaseMenu)` width: 100%; display: block; color: #E2E8F0 !important; - font-weight: 500; + font-weight: 400; + font-size: 13px; } .ant-menu-item.ant-menu-item-selected { background-color: rgba(56, 189, 248, 0.15); - font-weight: 600; + font-weight: 500; color: #ffffff !important; position: relative; box-sizing: border-box; @@ -86,19 +87,20 @@ export const Menu = styled(BaseMenu)` } .ant-menu-item { - border-radius: 8px; + border-radius: 6px; transition: all 0.3s ease; margin: 4px 10px; - padding: 0 14px !important; - height: 40px; - line-height: 40px; + padding: 0 12px !important; + height: 36px; + line-height: 36px; color: #E2E8F0 !important; - letter-spacing: 0.3px; + letter-spacing: 0.2px; text-align: left; + font-size: 13px; .anticon { - font-size: 18px; - margin-right: 12px; + font-size: 16px; + margin-right: 8px; color: #94A3B8; transition: all 0.2s ease; } @@ -120,7 +122,7 @@ export const Menu = styled(BaseMenu)` // Reset unwanted padding from ant design &.ant-menu-item-selected { - padding: 0 14px !important; + padding: 0 12px !important; } } @@ -136,10 +138,11 @@ export const Menu = styled(BaseMenu)` background: rgba(15, 23, 42, 0.6); margin: 2px 10px; border-radius: 6px; - padding: 0 14px !important; + padding: 0 12px !important; + font-size: 13px; .anticon { - font-size: 15px; + font-size: 14px; color: #94A3B8; } @@ -157,8 +160,8 @@ export const SectionDivider = styled.div` .ant-divider { color: #64748B; font-size: 10px; - font-weight: 600; - letter-spacing: 0.8px; + font-weight: 500; + letter-spacing: 0.6px; text-transform: uppercase; margin: 6px 0; @@ -171,7 +174,7 @@ export const SectionDivider = styled.div` export const SectionFooter = styled.div` margin-top: 12px; - padding: 10px; + padding: 8px; text-align: center; font-size: 11px; color: #64748B; @@ -183,6 +186,6 @@ export const SectionFooter = styled.div` margin-left: 6px; color: #475569; font-size: 10px; - letter-spacing: 0.5px; + letter-spacing: 0.4px; } `; diff --git a/src/components/layouts/main/sider/SiderMenu/SiderMenu.tsx b/src/components/layouts/main/sider/SiderMenu/SiderMenu.tsx index 1363e1a9..38555030 100644 --- a/src/components/layouts/main/sider/SiderMenu/SiderMenu.tsx +++ b/src/components/layouts/main/sider/SiderMenu/SiderMenu.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useParams } from 'react-router-dom'; import * as S from './SiderMenu.styles'; import { sidebarNavigation, SidebarNavigationItem } from '../sidebarNavigation'; import { Divider } from 'antd'; import { useAppSelector } from '@app/hooks/reduxHooks'; +import RepoNav from './RepoNav'; interface SiderContentProps { setCollapsed: (isCollapsed: boolean) => void; @@ -19,6 +20,7 @@ const sidebarNavFlat = sidebarNavigation.reduce( const SiderMenu: React.FC = ({ setCollapsed }) => { const { t } = useTranslation(); const location = useLocation(); + const { vendor, orgName, repoName } = useParams(); const user = useAppSelector((state) => state.user.user); const currentMenuItem = sidebarNavFlat.find(({ url }) => url === location.pathname); @@ -33,6 +35,9 @@ const SiderMenu: React.FC = ({ setCollapsed }) => { const appItems = sidebarNavigation.filter(item => item.section === 'app'); const userItems = sidebarNavigation.filter(item => item.section === 'user'); + // Check if we're in a repository context + const isRepoContext = Boolean(vendor && orgName && repoName); + // Function to create menu items const createMenuItems = (navItems: SidebarNavigationItem[]) => { return navItems.map((nav) => { @@ -58,6 +63,25 @@ const SiderMenu: React.FC = ({ setCollapsed }) => { return ( <> + {/* Repository-specific navigation when in repo context */} + {isRepoContext && vendor && orgName && repoName && ( + <> + + + + )} + + {/* Main app navigation */} theme.priceBg}; + transition: all 0.3s ease; + cursor: pointer; + + &.selected-plan { + border: 2px solid ${({ theme }) => theme.primary6}; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + } + + &.active-subscription { + border: 2px solid ${({ theme }) => theme.success}; + background: ${({ theme }) => theme.success}10; + } + + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); + } .ant-card-body { padding: 1.5rem; @@ -117,6 +135,28 @@ export const PricingCard = styled(BaseCard)` border-color: ${({ theme }) => theme.pricingTitle} !important; color: ${({ theme }) => theme.pricingTitle}; } + + button.selected-plan { + background-color: ${({ theme }) => theme.primary6}; + color: white; + border-color: ${({ theme }) => theme.primary6} !important; + } + + button.selected-plan:hover { + background-color: ${({ theme }) => theme.primary7}; + border-color: ${({ theme }) => theme.primary7} !important; + } + + button:disabled { + cursor: default !important; + opacity: 1; + } + + button.active-plan:disabled { + background-color: transparent; + color: ${({ theme }) => theme.pricingTitle}; + border-color: ${({ theme }) => theme.pricingTitle} !important; + } } `; diff --git a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/PaymentPricing.tsx b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/PaymentPricing.tsx index cd3e3d6f..c7d2a1f5 100644 --- a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/PaymentPricing.tsx +++ b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/PaymentPricing.tsx @@ -16,6 +16,7 @@ import { Flex } from 'antd'; import pricingData from './payment.json'; import { PaymentPricingContent } from './paymentPricingContent/PaymentPricingContent'; import { getPlanPrice, PlanTypes } from '@app/api/payment.api'; +import { useAppSelector } from '@app/hooks/reduxHooks'; export const PaymentPricing = () => { const location = useLocation(); @@ -23,9 +24,11 @@ export const PaymentPricing = () => { const { t } = useTranslation(); const { isTablet } = useResponsive(); + const user = useAppSelector((state) => state.user?.user); const [currency, setCurrency] = useState(searchParams.get('currency') || 'INR'); const [switchState, setSwithState] = useState(false); + const [selectedPlan, setSelectedPlan] = useState('freemium-plan'); const [exchangeRateUSDToINR, setExchangeRateUSDToINR] = useState(83); @@ -81,6 +84,22 @@ export const PaymentPricing = () => { } }, []); + // Initialize selectedPlan based on user's current plan type + useEffect(() => { + if (user?.planType) { + const planMapping: { [key: string]: string } = { + 'FREE': 'freemium-plan', + 'FREEMIUM': 'freemium-plan', + 'PREMIUM': 'basic-plan', + 'PRO': 'pro-plan', + 'ELITE': 'elite-plan' + }; + + const mappedPlan = planMapping[user.planType.toUpperCase()] || 'freemium-plan'; + setSelectedPlan(mappedPlan); + } + }, [user?.planType]); + const content = ( @@ -115,6 +134,8 @@ export const PaymentPricing = () => { setSwithState={setSwithState} currency={currency} exchangeRateUSDToINR={exchangeRateUSDToINR} + selectedPlan={selectedPlan} + setSelectedPlan={setSelectedPlan} /> ))} diff --git a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentContext/PaymentContext.tsx b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentContext/PaymentContext.tsx index bf1deba9..28d0f0ce 100644 --- a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentContext/PaymentContext.tsx +++ b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentContext/PaymentContext.tsx @@ -21,6 +21,8 @@ export interface PaymentContextProps { privateFeature: string[]; publicFeature: string[]; additionalFeature: string[]; + selectedPlan: string; + setSelectedPlan: (plan: string) => void; } const defaultPaymentContext: PaymentContextProps = { @@ -31,7 +33,7 @@ const defaultPaymentContext: PaymentContextProps = { planName: '', getCurrencySymbol: (prev) => prev, currency: '', - convertPrices: (prev) => 0, + convertPrices: () => 0, priceAmount: '', priceID: '', switchState: false, @@ -44,6 +46,8 @@ const defaultPaymentContext: PaymentContextProps = { privateFeature: [], publicFeature: [], additionalFeature: [], + selectedPlan: '', + setSelectedPlan: () => {}, }; export const PaymentContext = createContext(defaultPaymentContext); diff --git a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/PaymentPricingContent.tsx b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/PaymentPricingContent.tsx index 22509b85..bffbfed5 100644 --- a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/PaymentPricingContent.tsx +++ b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/PaymentPricingContent.tsx @@ -6,6 +6,8 @@ import { PlanPricingCard } from './planPricingCard/PlanPricingCard'; import { PlanCheckoutModal } from './planCheckoutModal/PlanCheckoutModal'; import { PaymentContext } from '../paymentContext/PaymentContext'; import { getCurrencySymbol } from '@app/utils/utils'; +import { cashfreeStandardCheckout } from '@app/api/cashfree.api'; +import { paypalStandardCheckout } from '@app/api/paypal.api'; type ProductPayPerMonthType = { planAmountPerMonth: string; @@ -30,6 +32,8 @@ type PaymentContentProps = { setSwithState: (prev: boolean) => void; currency: string; exchangeRateUSDToINR: number; + selectedPlan: string; + setSelectedPlan: (plan: string) => void; }; export const PaymentPricingContent: React.FC = ({ @@ -44,6 +48,8 @@ export const PaymentPricingContent: React.FC = ({ setSwithState, currency, exchangeRateUSDToINR, + selectedPlan, + setSelectedPlan, }) => { const [isCheckoutModelOpen, setIsCheckoutModalOpen] = useState(false); const [isSubscriptionEnabled, setIsSubscriptionEnabled] = useState(false); @@ -70,17 +76,34 @@ export const PaymentPricingContent: React.FC = ({ const priceID = switchState ? productPaymentPerYear.price_id_yearly : productPaymentPerMonth.price_id_monthly; const handlePlanClick = () => { - const openCheckoutModel = ['Premium', 'Pro'].indexOf(planName) >= 0; - setIsCheckoutModalOpen(openCheckoutModel); + if (currency === "INR") { + cashfreeStandardCheckout(priceID, checkedItems.join(",")) + } else { + paypalStandardCheckout(priceID, checkedItems.join(",")) + } }; - const id = useAppSelector((state) => state.user?.user?.id); + const user = useAppSelector((state) => state.user?.user); useEffect(() => { - if (id) { + if (user?.planType) { + // Map the user's planType to the corresponding className + const planMapping: { [key: string]: string } = { + 'FREE': 'freemium-plan', + 'FREEMIUM': 'freemium-plan', + 'PREMIUM': 'basic-plan', + 'PRO': 'pro-plan', + 'ELITE': 'elite-plan' + }; + + const mappedPlan = planMapping[user.planType.toUpperCase()] || 'freemium-plan'; + console.log('User plan type:', user.planType, 'Mapped to:', mappedPlan); + setSubscribedPlan(mappedPlan); + } else { + console.log('No user plan type found, defaulting to freemium-plan'); setSubscribedPlan('freemium-plan'); } - }, [id]); + }, [user?.planType]); useEffect(() => { const openCheckoutModel = planNameFromUrl === planName; @@ -110,6 +133,8 @@ export const PaymentPricingContent: React.FC = ({ privateFeature, publicFeature, additionalFeature, + selectedPlan, + setSelectedPlan, }; return ( diff --git a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/planPricingCard/PlanPricingCard.tsx b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/planPricingCard/PlanPricingCard.tsx index 22c7fd94..c7d577e7 100644 --- a/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/planPricingCard/PlanPricingCard.tsx +++ b/src/components/profile/profileCard/profileFormNav/nav/payments/paymentPricing/paymentPricingContent/planPricingCard/PlanPricingCard.tsx @@ -18,11 +18,43 @@ export const PlanPricingCard: React.FC = () => { privateFeature, publicFeature, additionalFeature, + selectedPlan, + setSelectedPlan, } = useContext(PaymentContext); + const handleCardClick = () => { + if (!isActiveSubscription) { + setSelectedPlan(className); + } + }; + + const handleButtonClick = (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent card click event + if (isActiveSubscription) { + // Do nothing for active plan + return; + } + if (isSelected) { + // If selected, trigger purchase + handlePlanClick(); + } else { + // If not selected, select the plan first + setSelectedPlan(className); + } + }; + + const isSelected = selectedPlan === className; + const isActiveSubscription = subscribedPlan === className; + return ( - - {planName} + + + {planName} + <> {planName !== 'Elite' ? ( @@ -42,11 +74,15 @@ export const PlanPricingCard: React.FC = () => { <> {planName !== 'Elite' ? ( - {subscribedPlan === className ? 'Active Plan' : 'Checkout'} + {isActiveSubscription ? 'Active Plan' : isSelected ? 'Buy' : 'Select Plan'} ) : null} diff --git a/src/components/router/AppRouter.tsx b/src/components/router/AppRouter.tsx index 233359b9..87f29841 100644 --- a/src/components/router/AppRouter.tsx +++ b/src/components/router/AppRouter.tsx @@ -23,6 +23,9 @@ import GoogleOauthPage from '@app/pages/AuthPages/GoogleOAuthPage/GoogleOAuthPag import GithubOAuthPage from '@app/pages/AuthPages/GithubOAuthPage/GithubOAuthPage'; import DocGenContentPage from '@app/pages/DashboardPages/DocGenDashboardPage/DocGenContentPage/DocGenContentPage'; import DocGenOrgPage from '@app/pages/DashboardPages/DocGenDashboardPage/DocGenOrgPage/DocGenOrgPage'; +import AnalyzePage from '@app/pages/DashboardPages/DocGenDashboardPage/RepoPages/AnalyzePage'; +import DocufyPage from '@app/pages/DashboardPages/DocGenDashboardPage/RepoPages/DocufyPage'; +import SecurityPage from '@app/pages/DashboardPages/DocGenDashboardPage/RepoPages/SecurityPage'; import { LocalHostLogin } from '../auth/LocalHostLogin/LocalHostLogin'; import { trackPageView } from '@app/config/mixpanel'; import { trackGAPageView } from '@app/config/ga'; @@ -129,6 +132,33 @@ export const AppRouter: React.FC = () => { } /> + + + + } + /> + + + + + } + /> + + + + + } + /> + { {showAlert && user.countRepoGen > 4 && ( = ({
- {orgName}/{repoName} + {decodeGitOrgName(orgName)}/{repoName} {planName} {repoDetails?.isPrivate ? ( Private @@ -197,14 +198,14 @@ export const RepoHeader: React.FC = ({ - } - href={`https://github.com/${orgName}/${repoName}`} - target="_blank" - > - View on GitHub - + } + href={`https://github.com/${decodeGitOrgName(orgName)}/${repoName}`} + target="_blank" + > + View on GitHub + {repoDetails?.gitAppMetaData?.siteUrl && ( = ({ + + + + + + + + + + + + API Documentation + + } + key="api" + > + API documentation content will appear here. + + + + + Markdown Docs + + } + key="markdown" + > + Markdown documentation content will appear here. + + + + + + + + ); +}; + +export default DocufyPage; diff --git a/src/pages/DashboardPages/DocGenDashboardPage/RepoPages/SecurityPage.tsx b/src/pages/DashboardPages/DocGenDashboardPage/RepoPages/SecurityPage.tsx new file mode 100644 index 00000000..2e12353c --- /dev/null +++ b/src/pages/DashboardPages/DocGenDashboardPage/RepoPages/SecurityPage.tsx @@ -0,0 +1,252 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { PageTitle } from '@app/components/common/PageTitle/PageTitle'; +import { DashboardWrapper } from '@app/components/dashboard/DashboardWrapper'; +import { + SafetyOutlined, + WarningOutlined, + LockOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + InfoCircleOutlined +} from '@ant-design/icons'; +import { Badge, Button, Card, Col, List, Row, Tag, Typography } from 'antd'; + +const { Text, Title, Paragraph } = Typography; + +// Mock security vulnerabilities data +const vulnerabilitiesData = [ + { + id: 1, + title: 'CVE-2023-44487 in axios@0.21.1', + severity: 'high', + path: 'package.json', + description: 'Axios before 1.6.0 allows attackers to cause a denial of service via a crafted HTTP/2 stream cancelation.', + status: 'open' + }, + { + id: 2, + title: 'SQL Injection Vulnerability', + severity: 'critical', + path: 'src/api/user.api.ts', + description: 'Potential SQL injection vulnerability in user input handling.', + status: 'open' + }, + { + id: 3, + title: 'Cross-Site Scripting (XSS)', + severity: 'medium', + path: 'src/components/common/UserInput.tsx', + description: 'Unvalidated user input could lead to XSS attacks.', + status: 'fixed' + }, + { + id: 4, + title: 'Authentication Bypass', + severity: 'low', + path: 'src/services/auth.service.ts', + description: 'Edge case in authentication logic could allow bypassing under specific conditions.', + status: 'open' + } +]; + +// Helper function for severity colors and icons +const getSeverityProps = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': + return { color: '#ff4d4f', icon: }; + case 'high': + return { color: '#fa8c16', icon: }; + case 'medium': + return { color: '#faad14', icon: }; + case 'low': + return { color: '#52c41a', icon: }; + default: + return { color: '#1890ff', icon: }; + } +}; + +const SecurityPage: React.FC = () => { + const { t } = useTranslation(); + const { orgName, repoName } = useParams(); + + return ( + <> + {t('sidebar.security')} + + + + + + <SafetyOutlined style={{ marginRight: 8, color: '#1890ff' }} /> + Security Insights: {orgName}/{repoName} + + + Comprehensive security analysis and vulnerability detection + + + + + + + + Security Score + + } + bordered={false} + > +
+
+ 76% +
+ + Your repository has a good security score, + but there are some issues that should be addressed. + + +
+
+ + + + + + Vulnerabilities Summary + + } + bordered={false} + extra={ + + } + > + + + + 1 + Critical + + + + + 1 + High + + + + + 1 + Medium + + + + + 1 + Low + + + + + + + + + + Security Vulnerabilities + + } + bordered={false} + > + { + const severityProps = getSeverityProps(item.severity); + + return ( + View Details, + item.status === 'open' ? + : + + ]} + > + + } + title={ +
+ {item.title} + + {item.severity} + + {item.status === 'fixed' && ( + + Fixed + + )} +
+ } + description={ +
+ + {item.path} + + + {item.description} + +
+ } + /> +
+ ); + }} + /> +
+ +
+
+ + ); +}; + +export default SecurityPage; diff --git a/src/pages/DashboardPages/JobsPage/JobsPage.tsx b/src/pages/DashboardPages/JobsPage/JobsPage.tsx index a890f113..44b85619 100644 --- a/src/pages/DashboardPages/JobsPage/JobsPage.tsx +++ b/src/pages/DashboardPages/JobsPage/JobsPage.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; +// import { useTranslation } from 'react-i18next'; import { PageTitle } from '@app/components/common/PageTitle/PageTitle'; import { DashboardWrapper } from '@app/components/dashboard/DashboardWrapper'; import { AllJobsList } from '@app/components/jobs/AllJobsList/AllJobsList'; const JobsPage: React.FC = () => { - const { t } = useTranslation(); + // const { t } = useTranslation(); return ( <> diff --git a/src/utils/gitNameEncoding.ts b/src/utils/gitNameEncoding.ts new file mode 100644 index 00000000..e42357b7 --- /dev/null +++ b/src/utils/gitNameEncoding.ts @@ -0,0 +1,2 @@ +export const decodeGitOrgName = (value: string): string => value.replace(/~~/g, '/'); +