]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10019 display effective deprecated version for domains
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 19 Dec 2018 09:22:32 +0000 (10:22 +0100)
committerSonarTech <sonartech@sonarsource.com>
Fri, 21 Dec 2018 19:21:01 +0000 (20:21 +0100)
14 files changed:
server/sonar-web/src/main/js/api/web-api.ts
server/sonar-web/src/main/js/app/types.d.ts
server/sonar-web/src/main/js/apps/web-api/components/Action.tsx
server/sonar-web/src/main/js/apps/web-api/components/ActionChangelog.tsx
server/sonar-web/src/main/js/apps/web-api/components/DeprecatedBadge.tsx
server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx
server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx
server/sonar-web/src/main/js/apps/web-api/components/Params.tsx
server/sonar-web/src/main/js/apps/web-api/components/ResponseExample.tsx
server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx
server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx
server/sonar-web/src/main/js/apps/web-api/components/__tests__/Params-test.tsx
server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap
server/sonar-web/src/main/js/apps/web-api/utils.ts

index 6c2cad03f2949c0d7827c87f73249e291e251255..f4b6e0ddaf1a075378ea89fd2db3085388e52e93 100644 (file)
 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<Domain[]> {
+export function fetchWebApi(showInternal = true): Promise<RawDomain[]> {
   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<Example> {
+export function fetchResponseExample(domain: string, action: string): Promise<T.WebApi.Example> {
   return getJSON('/api/webservices/response_example', { controller: domain, action }).catch(
     throwGlobalError
   );
index 6922707daf65729dd7f60b054f84689e28c160ee..0744cbcd765f9412c184f1c39b406a98093bf624 100644 (file)
@@ -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;
+    }
+  }
 }
index 9d205e3b5f6459d9332d9103face4c6571057652..c0e65191ee8945acc3a5ecb9c4b4c2cdc17626ed 100644 (file)
@@ -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<Props, State> {
 
   handleShowParamsClick = (e: React.SyntheticEvent<HTMLElement>) => {
     e.preventDefault();
-    this.setState({
+    this.setState(state => ({
       showChangelog: false,
       showResponse: false,
-      showParams: !this.state.showParams
-    });
+      showParams: !state.showParams
+    }));
   };
 
   handleShowResponseClick = (e: React.SyntheticEvent<HTMLElement>) => {
     e.preventDefault();
-    this.setState({
+    this.setState(state => ({
       showChangelog: false,
       showParams: false,
-      showResponse: !this.state.showResponse
-    });
+      showResponse: !state.showResponse
+    }));
   };
 
   handleChangelogClick = (e: React.SyntheticEvent<HTMLElement>) => {
     e.preventDefault();
-    this.setState({
-      showChangelog: !this.state.showChangelog,
+    this.setState(state => ({
+      showChangelog: !state.showChangelog,
       showParams: false,
       showResponse: false
-    });
+    }));
   };
 
   renderTabs() {
index 47e57ec9ebc9d2ab9791d8fc4014fdc039004e04..dfe35bd9a26b2cd76453e272461cb583cbb5b583 100644 (file)
  * 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) {
index 717291ef7de29025d4a1bd3929adf54c418e97db..8d94cfd592c0d024c6443f7b3c40f94f4845382d 100644 (file)
@@ -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 }) {
     </Tooltip>
   );
 }
-
-function parseVersion(version: string) {
-  const match = /(\d+)\.(\d+)/.exec(version);
-  if (match) {
-    return { major: Number(match[1]), minor: Number(match[2]) };
-  } else {
-    return undefined;
-  }
-}
index bf7798134fdb44bfbb06d6de46d6b8bd13f1e08e..eb60ef9127b0184976204ea9dcaa78c6f34c4c3d 100644 (file)
@@ -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) {
       <header className="web-api-domain-header">
         <h2 className="web-api-domain-title">{domain.path}</h2>
 
-        {domain.deprecated && (
+        {domain.deprecatedSince && (
           <span className="spacer-left">
-            <DeprecatedBadge />
+            <DeprecatedBadge since={domain.deprecatedSince} />
           </span>
         )}
 
index b45675c25e9d5657be109de6dc924a1689c3a531..9b689cb40c6217815ae07a1b1aa6e7c4d40edf63 100644 (file)
@@ -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 (
+      <Link
+        className={classNames('list-group-item', {
+          active: isDomainPathActive(domain.path, splat)
+        })}
+        key={domain.path}
+        to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}>
+        <h3 className="list-group-item-heading">
+          {domain.path}
+          {domain.deprecatedSince && <DeprecatedBadge since={domain.deprecatedSince} />}
+          {internal && <InternalBadge />}
+        </h3>
+      </Link>
+    );
+  };
+
   return (
     <div className="api-documentation-results panel">
-      <div className="list-group">
-        {filteredDomains.map(domain => (
-          <Link
-            className={classNames('list-group-item', {
-              active: isDomainPathActive(domain.path, splat)
-            })}
-            key={domain.path}
-            to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}>
-            <h3 className="list-group-item-heading">
-              {domain.path}
-              {domain.deprecated && <DeprecatedBadge />}
-              {domain.internal && <InternalBadge />}
-            </h3>
-          </Link>
-        ))}
-      </div>
+      <div className="list-group">{filteredDomains.map(renderDomain)}</div>
     </div>
   );
 }
index 26a191183280ca748c46f6ab98512730ea66e18a..b6412dbfb1cc06018b5e35703eed7cf933b438fb 100644 (file)
 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<Props> {
-  renderKey(param: Param) {
+  renderKey(param: T.WebApi.Param) {
     return (
       <td className="markdown" style={{ width: 180 }}>
         <code>{param.key}</code>
@@ -73,7 +72,7 @@ export default class Params extends React.PureComponent<Props> {
     );
   }
 
-  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 (
index e83c62e418562d427617c2c1d811d541447d44a7..384dfcb22e5db54996a057ac2ccb178480c38a5f 100644 (file)
  * 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<Props, State> {
index 9e055ea0372a4128fcdb3a8ab245575fcaf1fc9f..8cb09db5f21894f3306b27f93124852a908b599c 100644 (file)
 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<Props, State> {
@@ -62,13 +70,21 @@ class WebApiApp extends React.PureComponent<Props, State> {
     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<Props, State> {
 }
 
 export default withRouter(WebApiApp);
+
+/** Checks if all actions are deprecated, and returns the latest deprecated one */
+function getLatestDeprecatedAction(domain: Pick<T.WebApi.Domain, 'actions'>) {
+  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;
+}
index 5c2a39fb7bfd27170293dd4a084d1a7d9fce27b3..d053e07d61e536b968b67733c183d5110adb5867 100644 (file)
@@ -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(<Menu {...PROPS} domains={domains} query={SHOW_INTERNAL} />)).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(<Menu {...PROPS} domains={domains} />)).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(<Menu {...PROPS} domains={domains} query={SEARCH_FOO} />)).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(<Menu {...PROPS} domains={domains} query={SEARCH_BAR} />)).toMatchSnapshot();
index 66bb4e1755a9f309775b887288c3d8170fe275ee..039b34028e9598881e19f9d441c0f38f40f8b881 100644 (file)
@@ -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',
index b2479be343b1cf1237bf9ed0986c5f3d2546dae9..64b8d0e4bd2ca5511fa917fadcae773168c6bf7f 100644 (file)
@@ -154,6 +154,9 @@ exports[`should render deprecated domains 1`] = `
         className="list-group-item-heading"
       >
         bar
+        <DeprecatedBadge
+          since="5.0"
+        />
       </h3>
     </Link>
   </div>
index 0fabf35af391561e5b46f4c0b872952854d25b49..c4058d5c27d676d7414c11c6f9ec0c4775e7256e 100644 (file)
@@ -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;
+  }
+}