@@ -41,6 +41,8 @@ jest.mock('../../../api/issues'); | |||
jest.mock('../../../api/rules'); | |||
jest.mock('../../../api/users'); | |||
jest.mock('../../../components/intl/DateFromNow'); | |||
jest.mock('../../../components/SourceViewer/helpers/lines', () => { | |||
const lines = jest.requireActual('../../../components/SourceViewer/helpers/lines'); | |||
return { | |||
@@ -246,7 +248,7 @@ it('should correctly show measures for a project', async () => { | |||
['coverage', '2.0%'], | |||
['duplicated_lines_density', '2.0%'], | |||
].forEach(([domain, value]) => { | |||
expect(ui.measureValueCell(folderRow, domain, value, 1)).toBeInTheDocument(); | |||
expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument(); | |||
}); | |||
// index.tsx | |||
@@ -260,7 +262,7 @@ it('should correctly show measures for a project', async () => { | |||
['coverage', '—'], | |||
['duplicated_lines_density', '—'], | |||
].forEach(([domain, value]) => { | |||
expect(ui.measureValueCell(fileRow, domain, value, 1)).toBeInTheDocument(); | |||
expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument(); | |||
}); | |||
}); | |||
@@ -278,6 +280,7 @@ it('should correctly show new VS overall measures for Portfolios', async () => { | |||
children: [ | |||
{ | |||
component: mockComponent({ | |||
analysisDate: '2022-02-01', | |||
key: 'child1', | |||
name: 'Child 1', | |||
}), | |||
@@ -314,6 +317,7 @@ it('should correctly show new VS overall measures for Portfolios', async () => { | |||
['security_hotspots', 'C'], | |||
['Maintainability', 'C'], | |||
['ncloc', '3'], | |||
['last_analysis_date', '2022-02-01'], | |||
].forEach(([domain, value]) => { | |||
expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument(); | |||
}); | |||
@@ -327,6 +331,7 @@ it('should correctly show new VS overall measures for Portfolios', async () => { | |||
['security_hotspots', '—'], | |||
['Maintainability', '—'], | |||
['ncloc', '—'], | |||
['last_analysis_date', '—'], | |||
].forEach(([domain, value]) => { | |||
expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument(); | |||
}); | |||
@@ -375,7 +380,7 @@ function getPageObject(user: UserEvent) { | |||
newCodeBtn: byRole('button', { name: 'projects.view.new_code' }), | |||
overallCodeBtn: byRole('button', { name: 'projects.view.overall_code' }), | |||
measureRow: (name: string | RegExp) => byRole('row', { name, exact: false }), | |||
measureValueCell: (row: HTMLElement, name: string, value: string, offset = 0) => { | |||
measureValueCell: (row: HTMLElement, name: string, value: string) => { | |||
const i = Array.from(screen.getAllByRole('columnheader')).findIndex((c) => | |||
c.textContent?.includes(name) | |||
); | |||
@@ -386,7 +391,8 @@ function getPageObject(user: UserEvent) { | |||
} | |||
const { getAllByRole } = within(row); | |||
const cell = getAllByRole('cell').at(i + offset); | |||
const cell = getAllByRole('cell').at(i); | |||
if (cell?.textContent === value) { | |||
return cell; | |||
} |
@@ -29,6 +29,47 @@ | |||
margin-top: -50px; | |||
} | |||
.code-components .table-wrapper { | |||
margin: 0 20px; | |||
} | |||
.code-components table.data { | |||
table-layout: fixed; | |||
} | |||
.code-components table.data td { | |||
padding: 8px 6px; | |||
vertical-align: middle; | |||
} | |||
.code-components table.data th { | |||
padding-top: 24px; | |||
} | |||
.code-components table.data th, | |||
.code-components table.data td:not(.thin) { | |||
width: 84px; | |||
} | |||
.code-components table.data td.code-name-cell, | |||
.code-components table.data th.code-name-cell { | |||
width: auto; | |||
} | |||
.code-components table.data th.thin, | |||
.code-components table.data td.thin { | |||
width: 10px !important; | |||
} | |||
.code-components table.data tr.current-folder { | |||
border-bottom: 1px solid var(--barBorderColor); | |||
} | |||
.code-components table.data tr.current-folder td { | |||
padding-bottom: 16px !important; | |||
padding-top: 10px !important; | |||
} | |||
.code-breadcrumbs { | |||
display: flex; | |||
flex-wrap: wrap; | |||
@@ -56,19 +97,6 @@ | |||
display: none; | |||
} | |||
.code-components-cell { | |||
padding-left: calc(2 * var(--gridSize)) !important; | |||
box-sizing: border-box; | |||
} | |||
.code-components-rating-cell { | |||
width: 110px; | |||
} | |||
.code-name-cell { | |||
max-width: 0; | |||
} | |||
@media (max-width: 1200px) { | |||
.code-name-cell .badge { | |||
display: none; |
@@ -292,10 +292,12 @@ export class CodeApp extends React.Component<Props, State> { | |||
? translate('projects.page') | |||
: translate('code.page'); | |||
const isPortfolio = isPortfolioLike(qualifier); | |||
return ( | |||
<div className="page page-limited"> | |||
<A11ySkipTarget anchor="code_main" /> | |||
{!canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( | |||
{!canBrowseAllChildProjects && isPortfolio && ( | |||
<StyledAlert variant="warning" className="it__portfolio_warning"> | |||
<AlertContent> | |||
{translate('code_viewer.not_all_measures_are_shown')} | |||
@@ -358,6 +360,7 @@ export class CodeApp extends React.Component<Props, State> { | |||
rootComponent={component} | |||
selected={highlighted} | |||
newCodeSelected={newCodeSelected} | |||
showAnalysisDate={isPortfolio} | |||
/> | |||
</div> | |||
<ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} /> |
@@ -20,10 +20,10 @@ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { withScrollTo } from '../../../components/hoc/withScrollTo'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import { WorkspaceContext } from '../../../components/workspace/context'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { MetricType } from '../../../types/metrics'; | |||
import { ComponentMeasure as TypeComponentMeasure, Metric } from '../../../types/types'; | |||
import ComponentMeasure from './ComponentMeasure'; | |||
import ComponentName from './ComponentName'; | |||
@@ -41,6 +41,7 @@ interface Props { | |||
rootComponent: TypeComponentMeasure; | |||
selected?: boolean; | |||
newCodeSelected?: boolean; | |||
showAnalysisDate?: boolean; | |||
} | |||
export class Component extends React.PureComponent<Props> { | |||
@@ -57,6 +58,7 @@ export class Component extends React.PureComponent<Props> { | |||
rootComponent, | |||
selected = false, | |||
newCodeSelected, | |||
showAnalysisDate, | |||
} = this.props; | |||
const isFile = | |||
@@ -64,22 +66,19 @@ export class Component extends React.PureComponent<Props> { | |||
component.qualifier === ComponentQualifier.TestFile; | |||
return ( | |||
<tr className={classNames({ selected })}> | |||
<td className="blank" /> | |||
<tr className={classNames({ selected, 'current-folder': isBaseComponent })}> | |||
{canBePinned && ( | |||
<td className="thin nowrap"> | |||
{isFile && ( | |||
<span className="spacer-right"> | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<ComponentPin | |||
branchLike={branchLike} | |||
component={component} | |||
openComponent={openComponent} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
</span> | |||
<WorkspaceContext.Consumer> | |||
{({ openComponent }) => ( | |||
<ComponentPin | |||
branchLike={branchLike} | |||
component={component} | |||
openComponent={openComponent} | |||
/> | |||
)} | |||
</WorkspaceContext.Consumer> | |||
)} | |||
</td> | |||
)} | |||
@@ -99,24 +98,18 @@ export class Component extends React.PureComponent<Props> { | |||
</td> | |||
{metrics.map((metric) => ( | |||
<td | |||
className={classNames('thin', { | |||
'text-center': metric.type === MetricType.Rating, | |||
'nowrap text-right': metric.type !== MetricType.Rating, | |||
})} | |||
key={metric.key} | |||
> | |||
<div | |||
className={classNames({ | |||
'code-components-rating-cell': metric.type === MetricType.Rating, | |||
'code-components-cell': metric.type !== MetricType.Rating, | |||
})} | |||
> | |||
<ComponentMeasure component={component} metric={metric} /> | |||
</div> | |||
<td className="text-center" key={metric.key}> | |||
<ComponentMeasure component={component} metric={metric} /> | |||
</td> | |||
))} | |||
<td className="blank" /> | |||
{showAnalysisDate && isBaseComponent && <td />} | |||
{showAnalysisDate && !isBaseComponent && ( | |||
<td className="text-center"> | |||
{component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—'} | |||
</td> | |||
)} | |||
</tr> | |||
); | |||
} |
@@ -22,12 +22,13 @@ import * as React from 'react'; | |||
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { ComponentMeasure, Metric } from '../../../types/types'; | |||
import Component from './Component'; | |||
import ComponentsEmpty from './ComponentsEmpty'; | |||
import ComponentsHeader from './ComponentsHeader'; | |||
interface Props { | |||
interface ComponentsProps { | |||
baseComponent?: ComponentMeasure; | |||
branchLike?: BranchLike; | |||
components: ComponentMeasure[]; | |||
@@ -35,33 +36,39 @@ interface Props { | |||
rootComponent: ComponentMeasure; | |||
selected?: ComponentMeasure; | |||
newCodeSelected?: boolean; | |||
showAnalysisDate?: boolean; | |||
} | |||
const BASE_COLUMN_COUNT = 4; | |||
export function Components(props: ComponentsProps) { | |||
const { | |||
baseComponent, | |||
branchLike, | |||
components, | |||
rootComponent, | |||
selected, | |||
metrics, | |||
newCodeSelected, | |||
showAnalysisDate, | |||
} = props; | |||
export class Components extends React.PureComponent<Props> { | |||
render() { | |||
const { | |||
baseComponent, | |||
branchLike, | |||
components, | |||
rootComponent, | |||
selected, | |||
metrics, | |||
newCodeSelected, | |||
} = this.props; | |||
const canBePinned = | |||
baseComponent && | |||
![ | |||
ComponentQualifier.Application, | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.SubPortfolio, | |||
].includes(baseComponent.qualifier as ComponentQualifier); | |||
const colSpan = metrics.length + BASE_COLUMN_COUNT; | |||
const canBePinned = baseComponent && !['APP', 'VW', 'SVW'].includes(baseComponent.qualifier); | |||
return ( | |||
<table className="data boxed-padding zebra"> | |||
return ( | |||
<div className="big-spacer-bottom table-wrapper"> | |||
<table className="data zebra"> | |||
{baseComponent && ( | |||
<ComponentsHeader | |||
baseComponent={baseComponent} | |||
canBePinned={canBePinned} | |||
metrics={metrics.map((metric) => metric.key)} | |||
rootComponent={rootComponent} | |||
showAnalysisDate={showAnalysisDate} | |||
/> | |||
)} | |||
<tbody> | |||
@@ -77,14 +84,12 @@ export class Components extends React.PureComponent<Props> { | |||
metrics={metrics} | |||
rootComponent={rootComponent} | |||
newCodeSelected={newCodeSelected} | |||
showAnalysisDate={showAnalysisDate} | |||
/> | |||
<tr className="blank"> | |||
<td colSpan={3}> | |||
<hr className="null-spacer-top" /> | |||
</td> | |||
<td colSpan={colSpan}> | |||
<hr className="null-spacer-top" /> | |||
</td> | |||
<td | |||
colSpan={metrics.length + 1 + (canBePinned ? 1 : 0) + (showAnalysisDate ? 1 : 0)} | |||
/> | |||
</tr> | |||
</> | |||
)} | |||
@@ -103,10 +108,11 @@ export class Components extends React.PureComponent<Props> { | |||
component={component} | |||
hasBaseComponent={baseComponent !== undefined} | |||
key={getComponentMeasureUniqueKey(component)} | |||
metrics={this.props.metrics} | |||
metrics={metrics} | |||
previous={index > 0 ? list[index - 1] : undefined} | |||
rootComponent={rootComponent} | |||
newCodeSelected={newCodeSelected} | |||
showAnalysisDate={showAnalysisDate} | |||
selected={ | |||
selected && | |||
getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected) | |||
@@ -116,15 +122,10 @@ export class Components extends React.PureComponent<Props> { | |||
) : ( | |||
<ComponentsEmpty canBePinned={canBePinned} /> | |||
)} | |||
<tr className="blank"> | |||
<td colSpan={3} /> | |||
<td colSpan={colSpan} /> | |||
</tr> | |||
</tbody> | |||
</table> | |||
); | |||
} | |||
</div> | |||
); | |||
} | |||
export default withKeyboardNavigation(Components); |
@@ -28,10 +28,9 @@ export default function ComponentsEmpty({ canBePinned = true }: Props) { | |||
return ( | |||
<tr> | |||
{canBePinned && <td />} | |||
<td className="note" colSpan={2}> | |||
<td className="note" colSpan={10}> | |||
{translate('no_results')} | |||
</td> | |||
<td colSpan={10} /> | |||
</tr> | |||
); | |||
} |
@@ -17,16 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { isPortfolioLike } from '../../../types/component'; | |||
import { ComponentMeasure } from '../../../types/types'; | |||
interface Props { | |||
interface ComponentsHeaderProps { | |||
baseComponent?: ComponentMeasure; | |||
canBePinned?: boolean; | |||
metrics: string[]; | |||
rootComponent: ComponentMeasure; | |||
showAnalysisDate?: boolean; | |||
} | |||
const SHORT_NAME_METRICS = [ | |||
@@ -36,13 +37,9 @@ const SHORT_NAME_METRICS = [ | |||
'new_duplicated_lines_density', | |||
]; | |||
export default function ComponentsHeader({ | |||
baseComponent, | |||
canBePinned = true, | |||
metrics, | |||
rootComponent, | |||
}: Props) { | |||
const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier); | |||
export default function ComponentsHeader(props: ComponentsHeaderProps) { | |||
const { baseComponent, canBePinned = true, metrics, rootComponent, showAnalysisDate } = props; | |||
const isPortfolio = isPortfolioLike(rootComponent.qualifier); | |||
let columns: string[] = []; | |||
if (isPortfolio) { | |||
columns = [ | |||
@@ -51,8 +48,12 @@ export default function ComponentsHeader({ | |||
translate('portfolio.metric_domain.vulnerabilities'), | |||
translate('portfolio.metric_domain.security_hotspots'), | |||
translate('metric_domain.Maintainability'), | |||
translate('metric', 'ncloc', 'name'), | |||
translate('metric.ncloc.name'), | |||
]; | |||
if (showAnalysisDate) { | |||
columns.push(translate('code.last_analysis_date')); | |||
} | |||
} else { | |||
columns = metrics.map((metric) => | |||
translate('metric', metric, SHORT_NAME_METRICS.includes(metric) ? 'short_name' : 'name') | |||
@@ -62,23 +63,14 @@ export default function ComponentsHeader({ | |||
return ( | |||
<thead> | |||
<tr className="code-components-header"> | |||
<th className="thin nowrap" colSpan={canBePinned ? 2 : 1} /> | |||
<th /> | |||
{canBePinned && <th className="thin" aria-label={translate('code.pin')} />} | |||
<th className="code-name-cell" aria-label={translate('code.name')} /> | |||
{baseComponent && | |||
columns.map((column, index) => ( | |||
<th | |||
className={classNames('thin', { | |||
'code-components-cell': !isPortfolio && index > 0, | |||
nowrap: !isPortfolio, | |||
'text-center': isPortfolio && index < columns.length - 1, | |||
'text-right': !isPortfolio || index === columns.length - 1, | |||
})} | |||
key={column} | |||
> | |||
columns.map((column) => ( | |||
<th className="text-center" key={column}> | |||
{column} | |||
</th> | |||
))} | |||
<th /> | |||
</tr> | |||
</thead> | |||
); |
@@ -24,7 +24,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { Issue, Measure } from '../../../types/types'; | |||
interface SourceViewerWrapperProps { | |||
export interface SourceViewerWrapperProps { | |||
branchLike?: BranchLike; | |||
component: string; | |||
componentMeasures: Measure[] | undefined; |
@@ -0,0 +1,76 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import ComponentsServiceMock from '../../../../api/mocks/ComponentsServiceMock'; | |||
import IssuesServiceMock from '../../../../api/mocks/IssuesServiceMock'; | |||
import { mockLocation } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import SourceViewerWrapper, { SourceViewerWrapperProps } from '../SourceViewerWrapper'; | |||
jest.mock('../../../../api/components'); | |||
jest.mock('../../../../api/issues'); | |||
// The following 2 mocks are needed, because IssuesServiceMock mocks more than it should. | |||
// This should be removed once IssuesServiceMock is cleaned up. | |||
jest.mock('../../../../api/rules'); | |||
jest.mock('../../../../api/users'); | |||
const issuesHandler = new IssuesServiceMock(); | |||
const componentsHandler = new ComponentsServiceMock(); | |||
// eslint-disable-next-line testing-library/no-node-access | |||
const originalQuerySelector = document.querySelector; | |||
const scrollIntoView = jest.fn(); | |||
beforeAll(() => { | |||
Object.defineProperty(document, 'querySelector', { | |||
writable: true, | |||
value: () => ({ scrollIntoView }), | |||
}); | |||
}); | |||
afterAll(() => { | |||
Object.defineProperty(document, 'querySelector', { | |||
writable: true, | |||
value: originalQuerySelector, | |||
}); | |||
}); | |||
beforeEach(() => { | |||
issuesHandler.reset(); | |||
componentsHandler.reset(); | |||
}); | |||
it('should scroll to a line directly', async () => { | |||
renderSourceViewerWrapper(); | |||
await screen.findAllByText('function Test() {}'); | |||
expect(scrollIntoView).toHaveBeenCalled(); | |||
}); | |||
function renderSourceViewerWrapper(props: Partial<SourceViewerWrapperProps> = {}) { | |||
return renderComponent( | |||
<SourceViewerWrapper | |||
component="foo:index.tsx" | |||
componentMeasures={[]} | |||
location={mockLocation({ query: { line: '2' } })} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,10 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getBreadcrumbs, getChildren, getComponent } from '../../api/components'; | |||
import { getBreadcrumbs, getChildren, getComponent, getComponentData } from '../../api/components'; | |||
import { getBranchLikeQuery, isPullRequest } from '../../helpers/branch-like'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { isPortfolioLike } from '../../types/component'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../types/component'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import { Breadcrumb, ComponentMeasure } from '../../types/types'; | |||
import { | |||
@@ -128,7 +128,7 @@ export function getCodeMetrics( | |||
} | |||
return options.includeQGStatus ? metrics.concat(MetricKey.alert_status) : metrics; | |||
} | |||
if (qualifier === 'APP') { | |||
if (qualifier === ComponentQualifier.Application) { | |||
return [...APPLICATION_METRICS]; | |||
} | |||
if (showLeakMeasure(branchLike)) { | |||
@@ -162,7 +162,7 @@ function retrieveComponentBase( | |||
}); | |||
} | |||
export function retrieveComponentChildren( | |||
export async function retrieveComponentChildren( | |||
componentKey: string, | |||
qualifier: string, | |||
instance: { mounted: boolean }, | |||
@@ -181,20 +181,34 @@ export function retrieveComponentChildren( | |||
includeQGStatus: true, | |||
}); | |||
return getChildren(componentKey, metrics, { | |||
const result = await getChildren(componentKey, metrics, { | |||
ps: PAGE_SIZE, | |||
s: 'qualifier,name', | |||
...getBranchLikeQuery(branchLike), | |||
}) | |||
.then(prepareChildren) | |||
.then((r) => { | |||
if (instance.mounted) { | |||
addComponentChildren(componentKey, r.components, r.total, r.page); | |||
storeChildrenBase(r.components); | |||
storeChildrenBreadcrumbs(componentKey, r.components); | |||
}).then(prepareChildren); | |||
if (instance.mounted && isPortfolioLike(qualifier)) { | |||
await Promise.all( | |||
result.components.map((c) => getComponentData({ component: c.refKey || c.key })) | |||
).then( | |||
(data) => { | |||
data.forEach(({ component: { analysisDate } }, i) => { | |||
result.components[i].analysisDate = analysisDate; | |||
}); | |||
}, | |||
() => { | |||
// noop | |||
} | |||
return r; | |||
}); | |||
); | |||
} | |||
if (instance.mounted) { | |||
addComponentChildren(componentKey, result.components, result.total, result.page); | |||
storeChildrenBase(result.components); | |||
storeChildrenBreadcrumbs(componentKey, result.components); | |||
} | |||
return result; | |||
} | |||
function retrieveComponentBreadcrumbs( |
@@ -26,5 +26,5 @@ interface Props { | |||
} | |||
export default function DateFromNow({ children, date }: Props) { | |||
return children && children(date.toString()); | |||
return children ? children(date.toString()) : date.toString(); | |||
} |
@@ -113,6 +113,7 @@ export interface ComponentQualityProfile { | |||
} | |||
export interface ComponentMeasureIntern { | |||
analysisDate?: string; | |||
branch?: string; | |||
description?: string; | |||
isFavorite?: boolean; |
@@ -3385,6 +3385,9 @@ code.open_component_page=Open Component's Page | |||
code.search_placeholder=Search for files... | |||
code.search_placeholder.portfolio=Search for projects and sub-portfolios... | |||
code.parent_folder=Parent folder | |||
code.last_analysis_date=Last analysis | |||
code.name=Name | |||
code.pin=Pin file | |||
#------------------------------------------------------------------------------ |