From: Stas Vilchik Date: Wed, 19 Dec 2018 09:22:32 +0000 (+0100) Subject: SONAR-10019 display effective deprecated version for domains X-Git-Tag: 7.6~218 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=199c8e07b501626a4376fb445c90b36428a22442;p=sonarqube.git SONAR-10019 display effective deprecated version for domains --- diff --git a/server/sonar-web/src/main/js/api/web-api.ts b/server/sonar-web/src/main/js/api/web-api.ts index 6c2cad03f29..f4b6e0ddaf1 100644 --- a/server/sonar-web/src/main/js/api/web-api.ts +++ b/server/sonar-web/src/main/js/api/web-api.ts @@ -20,69 +20,22 @@ import { getJSON } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -export interface Changelog { - description: string; - version: string; -} - -export interface Param { - defaultValue?: string; - deprecatedKey?: string; - deprecatedKeySince?: string; - deprecatedSince?: string; - description: string; - exampleValue?: string; - internal: boolean; - key: string; - maximumLength?: number; - maximumValue?: number; - maxValuesAllowed?: number; - minimumLength?: number; - minimumValue?: number; - possibleValues?: string[]; - required: boolean; - since?: string; -} - -export interface Action { - key: string; - changelog: Changelog[]; - description: string; +interface RawDomain { + actions: T.WebApi.Action[]; deprecatedSince?: string; - hasResponseExample: boolean; - internal: boolean; - params?: Param[]; - post: boolean; - since?: string; -} - -export interface Domain { - actions: Action[]; description: string; - deprecated: boolean; internal: boolean; path: string; since?: string; } -export interface Example { - example: string; - format: string; -} - -export function fetchWebApi(showInternal = true): Promise { +export function fetchWebApi(showInternal = true): Promise { return getJSON('/api/webservices/list', { include_internals: showInternal }) - .then(r => - r.webServices.map((domain: any) => { - const deprecated = !domain.actions.find((action: any) => !action.deprecatedSince); - const internal = !domain.actions.find((action: any) => !action.internal); - return { ...domain, deprecated, internal }; - }) - ) + .then(r => r.webServices) .catch(throwGlobalError); } -export function fetchResponseExample(domain: string, action: string): Promise { +export function fetchResponseExample(domain: string, action: string): Promise { return getJSON('/api/webservices/response_example', { controller: domain, action }).catch( throwGlobalError ); diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index 6922707daf6..0744cbcd765 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -886,4 +886,56 @@ declare namespace T { id: string; success: boolean; } + + export namespace WebApi { + export interface Action { + key: string; + changelog: Changelog[]; + description: string; + deprecatedSince?: string; + hasResponseExample: boolean; + internal: boolean; + params?: Param[]; + post: boolean; + since?: string; + } + + export interface Changelog { + description: string; + version: string; + } + + export interface Domain { + actions: Action[]; + deprecatedSince?: string; + description: string; + internal?: boolean; + path: string; + since?: string; + } + + export interface Example { + example: string; + format: string; + } + + export interface Param { + defaultValue?: string; + deprecatedKey?: string; + deprecatedKeySince?: string; + deprecatedSince?: string; + description: string; + exampleValue?: string; + internal: boolean; + key: string; + maximumLength?: number; + maximumValue?: number; + maxValuesAllowed?: number; + minimumLength?: number; + minimumValue?: number; + possibleValues?: string[]; + required: boolean; + since?: string; + } + } } diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx index 9d205e3b5f6..c0e65191ee8 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx @@ -27,12 +27,11 @@ import DeprecatedBadge from './DeprecatedBadge'; import InternalBadge from './InternalBadge'; import { getActionKey } from '../utils'; import LinkIcon from '../../../components/icons-components/LinkIcon'; -import { Action as ActionType, Domain as DomainType } from '../../../api/web-api'; import { translate, translateWithParameters } from '../../../helpers/l10n'; interface Props { - action: ActionType; - domain: DomainType; + action: T.WebApi.Action; + domain: T.WebApi.Domain; showDeprecated: boolean; showInternal: boolean; } @@ -52,29 +51,29 @@ export default class Action extends React.PureComponent { handleShowParamsClick = (e: React.SyntheticEvent) => { e.preventDefault(); - this.setState({ + this.setState(state => ({ showChangelog: false, showResponse: false, - showParams: !this.state.showParams - }); + showParams: !state.showParams + })); }; handleShowResponseClick = (e: React.SyntheticEvent) => { e.preventDefault(); - this.setState({ + this.setState(state => ({ showChangelog: false, showParams: false, - showResponse: !this.state.showResponse - }); + showResponse: !state.showResponse + })); }; handleChangelogClick = (e: React.SyntheticEvent) => { e.preventDefault(); - this.setState({ - showChangelog: !this.state.showChangelog, + this.setState(state => ({ + showChangelog: !state.showChangelog, showParams: false, showResponse: false - }); + })); }; renderTabs() { diff --git a/server/sonar-web/src/main/js/apps/web-api/components/ActionChangelog.tsx b/server/sonar-web/src/main/js/apps/web-api/components/ActionChangelog.tsx index 47e57ec9ebc..dfe35bd9a26 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/ActionChangelog.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/ActionChangelog.tsx @@ -18,10 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Changelog } from '../../../api/web-api'; interface Props { - changelog: Changelog[]; + changelog: T.WebApi.Changelog[]; } export default function ActionChangelog({ changelog }: Props) { diff --git a/server/sonar-web/src/main/js/apps/web-api/components/DeprecatedBadge.tsx b/server/sonar-web/src/main/js/apps/web-api/components/DeprecatedBadge.tsx index 717291ef7de..8d94cfd592c 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/DeprecatedBadge.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/DeprecatedBadge.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { parseVersion } from '../utils'; export default function DeprecatedBadge({ since }: { since?: string }) { const version = since && parseVersion(since); @@ -35,12 +36,3 @@ export default function DeprecatedBadge({ since }: { since?: string }) { ); } - -function parseVersion(version: string) { - const match = /(\d+)\.(\d+)/.exec(version); - if (match) { - return { major: Number(match[1]), minor: Number(match[2]) }; - } else { - return undefined; - } -} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx index bf7798134fd..eb60ef9127b 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx @@ -22,10 +22,9 @@ import Action from './Action'; import DeprecatedBadge from './DeprecatedBadge'; import InternalBadge from './InternalBadge'; import { getActionKey, actionsFilter, Query } from '../utils'; -import { Domain as DomainType } from '../../../api/web-api'; interface Props { - domain: DomainType; + domain: T.WebApi.Domain; query: Query; } @@ -37,9 +36,9 @@ export default function Domain({ domain, query }: Props) {

{domain.path}

- {domain.deprecated && ( + {domain.deprecatedSince && ( - + )} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx index b45675c25e9..9b689cb40c6 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx @@ -23,10 +23,9 @@ import * as classNames from 'classnames'; import DeprecatedBadge from './DeprecatedBadge'; import InternalBadge from './InternalBadge'; import { isDomainPathActive, actionsFilter, Query, serializeQuery } from '../utils'; -import { Domain } from '../../../api/web-api'; interface Props { - domains: Domain[]; + domains: T.WebApi.Domain[]; query: Query; splat: string; } @@ -40,24 +39,27 @@ export default function Menu(props: Props) { }) .filter(domain => domain.filteredActions.length); + const renderDomain = (domain: T.WebApi.Domain) => { + const internal = !domain.actions.find(action => !action.internal); + return ( + +

+ {domain.path} + {domain.deprecatedSince && } + {internal && } +

+ + ); + }; + return (
-
- {filteredDomains.map(domain => ( - -

- {domain.path} - {domain.deprecated && } - {domain.internal && } -

- - ))} -
+
{filteredDomains.map(renderDomain)}
); } diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx index 26a19118328..b6412dbfb1c 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx @@ -20,17 +20,16 @@ import * as React from 'react'; import InternalBadge from './InternalBadge'; import DeprecatedBadge from './DeprecatedBadge'; -import { Param } from '../../../api/web-api'; import { translate, translateWithParameters } from '../../../helpers/l10n'; interface Props { - params: Param[]; + params: T.WebApi.Param[]; showDeprecated: boolean; showInternal: boolean; } export default class Params extends React.PureComponent { - renderKey(param: Param) { + renderKey(param: T.WebApi.Param) { return ( {param.key} @@ -73,7 +72,7 @@ export default class Params extends React.PureComponent { ); } - renderConstraint(param: Param, field: keyof Param, label: string) { + renderConstraint(param: T.WebApi.Param, field: keyof T.WebApi.Param, label: string) { const value = param[field]; if (value !== undefined) { return ( diff --git a/server/sonar-web/src/main/js/apps/web-api/components/ResponseExample.tsx b/server/sonar-web/src/main/js/apps/web-api/components/ResponseExample.tsx index e83c62e4185..384dfcb22e5 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/ResponseExample.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/ResponseExample.tsx @@ -18,20 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { - Action, - Domain, - Example, - fetchResponseExample as fetchResponseExampleApi -} from '../../../api/web-api'; +import { fetchResponseExample as fetchResponseExampleApi } from '../../../api/web-api'; interface Props { - action: Action; - domain: Domain; + action: T.WebApi.Action; + domain: T.WebApi.Domain; } interface State { - responseExample?: Example; + responseExample?: T.WebApi.Example; } export default class ResponseExample extends React.PureComponent { diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx index 9e055ea0372..8cb09db5f21 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx @@ -20,13 +20,21 @@ import * as React from 'react'; import Helmet from 'react-helmet'; import { Link, withRouter, WithRouterProps } from 'react-router'; +import { maxBy } from 'lodash'; import Domain from './Domain'; import Menu from './Menu'; import Search from './Search'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; -import { Domain as DomainType, fetchWebApi } from '../../../api/web-api'; -import { getActionKey, isDomainPathActive, Query, serializeQuery, parseQuery } from '../utils'; +import { fetchWebApi } from '../../../api/web-api'; +import { + getActionKey, + isDomainPathActive, + Query, + serializeQuery, + parseQuery, + parseVersion +} from '../utils'; import { translate } from '../../../helpers/l10n'; import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; import { scrollToElement } from '../../../helpers/scrolling'; @@ -35,7 +43,7 @@ import '../styles/web-api.css'; type Props = WithRouterProps; interface State { - domains: DomainType[]; + domains: T.WebApi.Domain[]; } class WebApiApp extends React.PureComponent { @@ -62,13 +70,21 @@ class WebApiApp extends React.PureComponent { fetchWebApi().then( domains => { if (this.mounted) { - this.setState({ domains }); + this.setState({ domains: this.parseDomains(domains) }); } }, () => {} ); } + parseDomains(domains: any[]): T.WebApi.Domain[] { + return domains.map(domain => { + const deprecated = getLatestDeprecatedAction(domain); + const internal = !domain.actions.find((action: any) => !action.internal); + return { ...domain, deprecatedSince: deprecated && deprecated.deprecatedSince, internal }; + }); + } + scrollToAction = () => { const splat = this.props.params.splat || ''; const action = document.getElementById(splat); @@ -177,3 +193,18 @@ class WebApiApp extends React.PureComponent { } export default withRouter(WebApiApp); + +/** Checks if all actions are deprecated, and returns the latest deprecated one */ +function getLatestDeprecatedAction(domain: Pick) { + const noVersion = { major: 0, minor: 0 }; + const allActionsDeprecated = domain.actions.every( + ({ deprecatedSince }) => deprecatedSince !== undefined + ); + const latestDeprecation = + allActionsDeprecated && + (maxBy(domain.actions, action => { + const version = (action.deprecatedSince && parseVersion(action.deprecatedSince)) || noVersion; + return version.major * 1024 + version.minor; + }) as T.WebApi.Action); + return latestDeprecation || undefined; +} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx index 5c2a39fb7bf..d053e07d61e 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import Menu from '../Menu'; -const ACTION = { +const ACTION: T.WebApi.Action = { key: 'foo', changelog: [], description: 'Foo Desc', @@ -29,19 +29,15 @@ const ACTION = { internal: false, post: false }; -const DOMAIN1 = { +const DOMAIN1: T.WebApi.Domain = { actions: [ACTION], path: 'foo', - description: 'API Foo', - deprecated: false, - internal: false + description: 'API Foo' }; -const DOMAIN2 = { +const DOMAIN2: T.WebApi.Domain = { actions: [ACTION], path: 'bar', - description: 'API Bar', - deprecated: false, - internal: false + description: 'API Bar' }; const PROPS = { domains: [DOMAIN1, DOMAIN2], @@ -55,7 +51,7 @@ const SEARCH_FOO = { search: 'Foo', deprecated: false, internal: false }; const SEARCH_BAR = { search: 'Bar', deprecated: false, internal: false }; it('should render deprecated domains', () => { - const domain = { + const domain: T.WebApi.Domain = { ...DOMAIN2, deprecatedSince: '5.0', actions: [{ ...ACTION, deprecatedSince: '5.0' }] @@ -65,7 +61,7 @@ it('should render deprecated domains', () => { }); it('should not render deprecated domains', () => { - const domain = { + const domain: T.WebApi.Domain = { ...DOMAIN2, deprecatedSince: '5.0', actions: [{ ...ACTION, deprecatedSince: '5.0' }] @@ -75,32 +71,40 @@ it('should not render deprecated domains', () => { }); it('should render internal domains', () => { - const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] }; + const domain: T.WebApi.Domain = { + ...DOMAIN2, + internal: true, + actions: [{ ...ACTION, internal: true }] + }; const domains = [DOMAIN1, domain]; expect(shallow()).toMatchSnapshot(); }); it('should not render internal domains', () => { - const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] }; + const domain: T.WebApi.Domain = { + ...DOMAIN2, + internal: true, + actions: [{ ...ACTION, internal: true }] + }; const domains = [DOMAIN1, domain]; expect(shallow()).toMatchSnapshot(); }); it('should render only domains with an action matching the query', () => { - const domain = { + const domain: T.WebApi.Domain = { ...DOMAIN2, - actions: [{ ...ACTION, key: 'bar', path: 'bar', description: 'Bar Desc' }] + actions: [{ ...ACTION, key: 'bar', description: 'Bar Desc' }] }; const domains = [DOMAIN1, domain]; expect(shallow()).toMatchSnapshot(); }); it('should also render domains with an actions description matching the query', () => { - const domain = { + const domain: T.WebApi.Domain = { ...DOMAIN1, path: 'baz', description: 'API Baz', - actions: [{ ...ACTION, key: 'baz', path: 'baz', description: 'barbaz' }] + actions: [{ ...ACTION, key: 'baz', description: 'barbaz' }] }; const domains = [DOMAIN1, DOMAIN2, domain]; expect(shallow()).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Params-test.tsx b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Params-test.tsx index 66bb4e1755a..039b34028e9 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Params-test.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Params-test.tsx @@ -20,9 +20,8 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import Params from '../Params'; -import { Param } from '../../../../api/web-api'; -const DEFAULT_PARAM = { +const DEFAULT_PARAM: T.WebApi.Param = { key: 'foo', description: 'Foo desc', internal: false, @@ -57,7 +56,7 @@ it('should render deprecated key', () => { }); it('should render different value constraints', () => { - const param: Param = { + const param: T.WebApi.Param = { ...DEFAULT_PARAM, defaultValue: 'def', exampleValue: 'foo', diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap index b2479be343b..64b8d0e4bd2 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap @@ -154,6 +154,9 @@ exports[`should render deprecated domains 1`] = ` className="list-group-item-heading" > bar + diff --git a/server/sonar-web/src/main/js/apps/web-api/utils.ts b/server/sonar-web/src/main/js/apps/web-api/utils.ts index 0fabf35af39..c4058d5c27d 100644 --- a/server/sonar-web/src/main/js/apps/web-api/utils.ts +++ b/server/sonar-web/src/main/js/apps/web-api/utils.ts @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { memoize } from 'lodash'; -import { Domain, Action } from '../../api/web-api'; import { cleanQuery, RawQuery, @@ -33,7 +32,7 @@ export interface Query { internal: boolean; } -export function actionsFilter(query: Query, domain: Domain, action: Action) { +export function actionsFilter(query: Query, domain: T.WebApi.Domain, action: T.WebApi.Action) { const lowSearchQuery = query.search.toLowerCase(); return ( (query.internal || !action.internal) && @@ -80,3 +79,12 @@ export const serializeQuery = memoize( internal: query.internal || undefined }) ); + +export function parseVersion(version: string) { + const match = /(\d+)\.(\d+)/.exec(version); + if (match) { + return { major: Number(match[1]), minor: Number(match[2]) }; + } else { + return undefined; + } +}