Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

THREESCALE-3927 Update Swagger to Open API 3.0 #3103

Merged
merged 89 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
6431713
THREESCALE-3927 Added rswag gem
nidhi-soni1104 Nov 9, 2022
917c5c6
THREESCALE-3927 update config to generate json file
nidhi-soni1104 Nov 10, 2022
f6c8f27
THREESCALE-3927 converted to openapi 3.0
nidhi-soni1104 Nov 16, 2022
60f10bf
THREESCALE-3927 removed rswag gem completely
nidhi-soni1104 Dec 12, 2022
3474cd7
THREESCALE-3927 removed unwanted space and extra file
nidhi-soni1104 Dec 12, 2022
85291b4
THREESCALE-3927 Converted api to openapi3.0 format and listed on the UI
nidhi-soni1104 Dec 15, 2022
0bd1736
THREESCALE-3927 make api listing dynamic
nidhi-soni1104 Jan 4, 2023
320b866
THREESCALE-3927 Fixed tags and UI
nidhi-soni1104 Jan 5, 2023
19fcdd1
THREESCALE-3927 make id divs dynamic
nidhi-soni1104 Jan 11, 2023
743ac7e
THREESCALE-3927 added space
nidhi-soni1104 Jan 11, 2023
dd7698a
OpenAPI3: Exclude forbidden endpoints
jlledom Jan 12, 2023
4d1f295
THREESCALE-3927 removed unsed files
nidhi-soni1104 Jan 16, 2023
0ac7771
Revert "THREESCALE-3927 removed unsed files"
nidhi-soni1104 Jan 16, 2023
f25a892
THREESCALE-3927 Fixed failed integration test
nidhi-soni1104 Jan 23, 2023
cf33563
THREESCALE-3927 Fixed failed tests
nidhi-soni1104 Jan 23, 2023
fd5658e
Restore the original content of the Swagger 2.0 API specs
mayorova Jan 27, 2023
8590547
Add title and description for all API specs
mayorova Jan 27, 2023
6ccf2a8
Reorder and add tags Acc Mgmt API
mayorova Jan 30, 2023
35cd821
Reorder and tag Dev Portal API
mayorova Jan 30, 2023
871939d
THREESCALE-3927 Fixed test cases
nidhi-soni1104 Jan 31, 2023
f9a1d4d
THREESCALE-8394 code clean
nidhi-soni1104 Jan 31, 2023
d029760
THREESCALE-3927 added assertionf for open api version
nidhi-soni1104 Jan 31, 2023
37279ec
THREESCALE-3927 Added Tags in the API
nidhi-soni1104 Feb 1, 2023
e289688
THREESCALE-3927 added assertion to check API version
nidhi-soni1104 Feb 1, 2023
7af296e
Update Service Management API
mayorova Feb 1, 2023
2542482
THREESCALE-3927 removed extra space and breakpoint
nidhi-soni1104 Feb 1, 2023
f1462ae
Service Management API: transactions in request body
mayorova Feb 2, 2023
60b6c15
THREESCALE-3927 Fixed permission api
nidhi-soni1104 Feb 6, 2023
07c9060
THREESCALE-3927 added required space at the last
nidhi-soni1104 Feb 6, 2023
4b54cf2
THREESCALE-3927 make enum for array of strings
nidhi-soni1104 Feb 7, 2023
8c22c5a
THREESCALE-3927 fixed service management api url issue
nidhi-soni1104 Feb 9, 2023
05929bb
Some fixes for the Account Management API
mayorova Feb 9, 2023
2cd854b
THREESCALE-3927 updated PUT API params in request body
nidhi-soni1104 Feb 13, 2023
dc6b27c
THREESCALE-3927 converted post API into request body format
nidhi-soni1104 Feb 14, 2023
24af41b
THREESCALE-3927 Updated Accounts API's of Account Management API
nidhi-soni1104 Feb 15, 2023
5e4cdf5
THREESCALE-3927 Updated Account Applications API's of AMI
nidhi-soni1104 Feb 15, 2023
b332e1a
THREESCALE-3927 updated Api's
nidhi-soni1104 Feb 21, 2023
0df1c24
THREESCALE-3927 covnverted API parameters passsing to new correct format
nidhi-soni1104 Feb 27, 2023
b7658fc
THREESCALE Updated patch api
nidhi-soni1104 Feb 27, 2023
4daad6d
THREESCALE-3927 Fixed failed integration tests
nidhi-soni1104 Feb 28, 2023
11dc573
THREESCALE-3927 fixed conflicts
nidhi-soni1104 Mar 1, 2023
d69be1c
THREESCALE-3927 Fixed lint issues
nidhi-soni1104 Mar 1, 2023
2ba20d4
THREESCALE-3927 fixed few lint issues
nidhi-soni1104 Mar 1, 2023
dd88183
THREESCALE-3927 rearranged importing sequence
nidhi-soni1104 Mar 1, 2023
3015583
THREESCALE-3927 added new lines, updated cms api to new version and f…
nidhi-soni1104 Mar 1, 2023
1a39f52
THREESCALE-3927 updated remainin api syntaxes
nidhi-soni1104 Mar 1, 2023
a0f20ee
Fixes and updates for Acc Mgmt API spec
mayorova Mar 2, 2023
2cc5d0b
Fix styles in Swagger UI v3
mayorova Mar 3, 2023
dd43896
THREESCALE-3927 removed servers from response body
nidhi-soni1104 Mar 6, 2023
64aa8f0
Revert "THREESCALE-3927 removed servers from response body"
nidhi-soni1104 Mar 6, 2023
8d92098
Some fixes and updates for Analytics, Billing and Policy Registry specs
mayorova Mar 6, 2023
b7d8f01
Some fixes and update for Master API
mayorova Mar 6, 2023
47af16b
Fixes and updates for CMS API
mayorova Mar 6, 2023
63010ac
Restore newline at the end of file
mayorova Mar 6, 2023
1b82073
THREESCALE-3927 Fixed Account Management API autocompletes
nidhi-soni1104 Mar 9, 2023
d0e290b
THREESCALE-3927 Updated Analytics API autocomplete
nidhi-soni1104 Mar 9, 2023
95fee48
THREESCALE-3927 Billing API autocomplete
nidhi-soni1104 Mar 9, 2023
be40a4c
THREESCALE-3927 Master API autocomplete
nidhi-soni1104 Mar 9, 2023
ba46c19
THREESCALE-3927 Fixed Developer Portal API
nidhi-soni1104 Mar 9, 2023
45c28e9
THREESCALE-3927 Policy Registry API
nidhi-soni1104 Mar 9, 2023
7318724
THREESCALE-3927 Service Management API autocomplete
nidhi-soni1104 Mar 9, 2023
3f11557
Merge branch 'master' into THREESCALE-3927-update-to-openapi
mayorova Mar 10, 2023
fad2c25
Fixups
mayorova Mar 10, 2023
92810fa
Enable TryItOut by default to improve UX
mayorova Mar 10, 2023
3dc142a
Update controller to include the full URL in servers list in the API …
mayorova Mar 15, 2023
12272ae
Refactor provider active docs
mayorova Mar 15, 2023
126fa27
Fix service token auto-complete
mayorova Mar 15, 2023
b400392
Remove access token from active docs autocomplete
mayorova Mar 15, 2023
ee90142
Auto-complete fixes
mayorova Mar 15, 2023
de22cf2
Small fixes after review
mayorova Mar 16, 2023
532deeb
Rename ActiveDocs specification files
mayorova Mar 16, 2023
81a49b9
Do not include ApplicationHelper
mayorova Mar 17, 2023
1cbd7ea
Update CMS API according to the latest changes
mayorova Mar 27, 2023
b858333
Improve checking for API spec calls for autocomplete
mayorova Mar 27, 2023
eb07699
Merge branch 'master' into THREESCALE-3927-update-to-openapi
mayorova Mar 31, 2023
3bee640
Merge branch 'master' into THREESCALE-3927-update-to-openapi
mayorova Apr 13, 2023
3985412
Serialize Service Mgmt API's report endpoint body correctly
mayorova Mar 29, 2023
9e39a67
Some small refactoring
mayorova Apr 12, 2023
ffa3f34
Tiny update
mayorova Apr 12, 2023
14ac7f0
Remove cast to BodyValue
mayorova Apr 13, 2023
1045e25
Split objectToFormData into two functions
mayorova Apr 13, 2023
f2c9ac4
🏷️ move swagger-client types to d.ts
josemigallas Apr 13, 2023
1f56654
✅ fixes test
josemigallas Apr 13, 2023
78503bd
Merge pull request #3294 from 3scale/fix-report-body-serialization
mayorova Apr 13, 2023
c420580
Upgrade swagger-ui and swagger-client
mayorova Apr 14, 2023
0700720
Improve documentation to better guide customers and link to the schemas
mayorova Apr 19, 2023
e6a7755
Get rid of multiple account_data calls for 3scale docs
mayorova Apr 20, 2023
02ef45a
Fix autocomplete tests
mayorova Apr 20, 2023
8200ed9
Do not export autocompleteOAS3
mayorova Apr 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions app/controllers/api_docs/services_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def initialize(name, system_name)

def json
parsed_content = JSON.parse(file_content)
parsed_content['basePath'] = backend_base_host if backend_api?
parsed_content['servers'] = [{ 'url' => backend_base_host }] if backend_api?

parsed_content
end
Expand All @@ -54,9 +54,9 @@ def file_path

def file_name
if onpremises_version? && onpremises_version_preferred?
"#{name} (on-premises)"
"#{system_name}_on_premises"
else
name
system_name
end
end

Expand Down Expand Up @@ -115,18 +115,15 @@ def index
def show
system_name = params[:id].to_sym
api_file = (api_files.fetch(system_name) { raise ActiveRecord::RecordNotFound }).dup
api_file['apis'] = exclude_forbidden_endpoints(api_file['apis'])
api_file['paths'] = exclude_forbidden_endpoints(api_file['paths']) if master_on_premises?

render json: api_file
end

private

def exclude_forbidden_endpoints(apis)
apis.select do |api|
path = api['path']
!master_on_premises? || path.exclude?('plan')
end
def exclude_forbidden_endpoints(paths)
paths.select { |url| url.exclude?('plan') }
end

def allowed_api?(api)
Expand Down
19 changes: 13 additions & 6 deletions app/javascript/packs/active_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@
import SwaggerUI from 'swagger-ui'
import 'swagger-ui/dist/swagger-ui.css'

import { autocompleteInterceptor } from 'ActiveDocs/OAS3Autocomplete'
import { autocompleteRequestInterceptor } from 'ActiveDocs/OAS3Autocomplete'
import { fetchData } from 'utilities/fetchData'

import type { AccountDataResponse } from 'Types/SwaggerTypes'

import 'ActiveDocs/swagger-ui-3-patch.scss'

const accountDataUrl = '/api_docs/account_data.json'

window.SwaggerUI = (args: SwaggerUI.SwaggerUIOptions, serviceEndpoint: string) => {
const responseInterceptor = (response: SwaggerUI.Response) => autocompleteInterceptor(response, accountDataUrl, serviceEndpoint, args.url)
fetchData<AccountDataResponse>(accountDataUrl)
.then(accountData => {
const requestInterceptor = (request: SwaggerUI.Request) => autocompleteRequestInterceptor(request, accountData, serviceEndpoint)

SwaggerUI({
...args,
responseInterceptor
} as SwaggerUI.SwaggerUIOptions)
return SwaggerUI({
...args,
requestInterceptor
} as SwaggerUI.SwaggerUIOptions)
})
.catch(error => { console.error(error) })
}
24 changes: 24 additions & 0 deletions app/javascript/packs/provider_active_docs.ts
jlledom marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// We can define the 3scale plugins here and export the modified bundle
import 'swagger-ui/dist/swagger-ui.css'

import { renderSwaggerUI } from 'ActiveDocs/ThreeScaleApiDocs'

import 'ActiveDocs/swagger-ui-3-provider-patch.scss'

const renderActiveDocs = async () => {
const containerId = 'api-containers'
const container = document.getElementById(containerId)

if (!container) {
console.error(`The target element with ID ${containerId} was not found`)
return
}

const { baseUrl = '', apiDocsPath = '', apiDocsAccountDataPath = '' } = container.dataset

await renderSwaggerUI(container, apiDocsPath, baseUrl, apiDocsAccountDataPath)
}

document.addEventListener('DOMContentLoaded', () => {
renderActiveDocs().catch(error => { console.error(error) })
})
20 changes: 15 additions & 5 deletions app/javascript/packs/service_active_docs.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import SwaggerUI from 'swagger-ui'
import 'swagger-ui/dist/swagger-ui.css'

import { autocompleteInterceptor } from 'ActiveDocs/OAS3Autocomplete'
import { autocompleteRequestInterceptor } from 'ActiveDocs/OAS3Autocomplete'

import type { AccountDataResponse } from 'Types/SwaggerTypes'

import 'ActiveDocs/swagger-ui-3-provider-patch.scss'
import { fetchData } from '../src/utilities/fetchData'

document.addEventListener('DOMContentLoaded', () => {
const renderActiveDocs = async () => {
const containerId = 'swagger-ui-container'
const DATA_URL = 'p/admin/api_docs/account_data.json'
const container = document.getElementById(containerId)

if (!container) {
throw new Error('The target ID was not found: ' + containerId)
console.error(`Element with ID ${containerId} not found`)
return
}
const { url = '', baseUrl = '', serviceEndpoint = '' } = container.dataset
const accountDataUrl = `${baseUrl}${DATA_URL}`

const responseInterceptor: SwaggerUI.SwaggerUIOptions['responseInterceptor'] = (response) => autocompleteInterceptor(response, accountDataUrl, serviceEndpoint, url)
const accountData: AccountDataResponse = await fetchData<AccountDataResponse>(accountDataUrl)

const requestInterceptor: SwaggerUI.SwaggerUIOptions['requestInterceptor'] = (request) => autocompleteRequestInterceptor(request, accountData, serviceEndpoint)

SwaggerUI({
url,
// eslint-disable-next-line @typescript-eslint/naming-convention -- SwaggerUI API
dom_id: `#${containerId}`,
responseInterceptor
requestInterceptor
})
}

document.addEventListener('DOMContentLoaded', () => {
renderActiveDocs().catch(error => { console.error(error) })
})
50 changes: 30 additions & 20 deletions app/javascript/src/ActiveDocs/OAS3Autocomplete.ts
jlledom marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,29 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* TODO: this module needs to be properly typed !!! */

import { fetchData } from 'utilities/fetchData'

import type { Response as SwaggerUIResponse } from 'swagger-ui'
import type { AccountData } from 'Types/SwaggerTypes'
import type { Request as SwaggerUIRequest, Response as SwaggerUIResponse } from 'swagger-ui'
import type { AccountData, AccountDataResponse } from 'Types/SwaggerTypes'

const X_DATA_ATTRIBUTE = 'x-data-threescale-name'

const X_DATA_PARAMS_DESCRIPTIONS = {
user_keys: 'First user key from latest 5 applications',
metric_names: 'Latest 5 metrics',
metric_ids: 'Latest 5 metrics',
app_keys: 'First application key from the latest five applications',
app_ids: 'Latest 5 applications (across all accounts and services)',
app_keys: 'First application key from the latest five applications'
application_ids: 'Latest 5 applications',
user_keys: 'First user key from latest 5 applications',
account_ids: 'Latest 5 accounts',
access_token: 'Access Token',
user_ids: 'First user (admin) of the latest 5 account',
service_ids: 'Service IDs',
service_tokens: 'Service tokens',
admin_ids: 'Latest 5 users (admin) from your account',
service_plan_ids: 'Latest 5 service plans',
application_plan_ids: 'Latest 5 application plans',
account_plan_ids: 'Latest 5 account plans',
client_ids: 'Client IDs from the latest five applications',
client_secrets: 'Client secrets from the latest five applications'
} as const

const addAutocompleteToParam = (param: any, accountData: AccountData): any => {
Expand All @@ -34,7 +46,7 @@ const addAutocompleteToParam = (param: any, accountData: AccountData): any => {
? {
...param,
examples: autocompleteData.reduce<{ summary: string; value: string }[]>((examples, item) => (
[...examples, { summary: item.name, value: item.value }]
[...examples, { summary: item.value ? `${item.name} - ${item.value}` : item.name, value: item.value }]
), [{ summary: X_DATA_PARAMS_DESCRIPTIONS[xDataKey], value: '-' }])
}
: param
Expand Down Expand Up @@ -100,14 +112,13 @@ export interface Response extends SwaggerUIResponse {
text: string;
}

const autocompleteOAS3 = async (response: SwaggerUIResponse, accountDataUrl: string, serviceEndpoint: string): Promise<Response> => {
const autocompleteOAS3 = (response: SwaggerUIResponse, accountData: AccountDataResponse, serviceEndpoint: string): Response => {
const bodyWithServer = injectServerToResponseBody(response.body, serviceEndpoint)
const data = await fetchData<{ results: AccountData }>(accountDataUrl)

let body = undefined
try {
body = data.results
? injectAutocompleteToResponseBody(bodyWithServer, data.results)
body = accountData.results
? injectAutocompleteToResponseBody(bodyWithServer, accountData.results)
: bodyWithServer
} catch (error: unknown) {
console.error(error)
Expand All @@ -123,17 +134,16 @@ const autocompleteOAS3 = async (response: SwaggerUIResponse, accountDataUrl: str
}

/**
* Intercept and process the response made by Swagger UI
* Apply transformations (inject servers list and autocomplete data) to the response for OpenAPI spec requests, and
* Intercept the request, and if it fetches the OpenAPI specification, apply a custom responseInterceptor that applies
* transformations (inject servers list and autocomplete data) to the response
* keep the responses to the actual API calls (made through 'Try it out') untouched
* @param response response to the request made through Swagger UI
* @param specUrl URL of the OpenAPI specification
* @param accountDataUrl URL of the data for autocompletion
* @param request request made through Swagger UI
* @param accountData data for autocompletion
* @param serviceEndpoint Public Base URL of the gateway, that will replace the URL in the "servers" object
*/
export const autocompleteInterceptor = (response: SwaggerUIResponse, accountDataUrl: string, serviceEndpoint: string, specUrl?: string): Promise<Response> | SwaggerUIResponse => {
if (!response.url.includes(specUrl)) {
return response
export const autocompleteRequestInterceptor = (request: SwaggerUIRequest, accountData: AccountDataResponse, serviceEndpoint: string): Promise<SwaggerUIRequest> | SwaggerUIRequest => {
if (request.loadSpec) {
request.responseInterceptor = (response: SwaggerUIResponse) => autocompleteOAS3(response, accountData, serviceEndpoint)
}
return autocompleteOAS3(response, accountDataUrl, serviceEndpoint)
return request
}
141 changes: 141 additions & 0 deletions app/javascript/src/ActiveDocs/ThreeScaleApiDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import SwaggerUI from 'swagger-ui'
// this is how SwaggerUI imports this function https://github.com/swagger-api/swagger-ui/pull/6208
import { execute } from 'swagger-client/es/execute'

import { fetchData } from 'utilities/fetchData'
import { safeFromJsonString } from 'utilities/json-utils'
import { autocompleteRequestInterceptor } from 'ActiveDocs/OAS3Autocomplete'

import type { AccountDataResponse, ApiDocsServices, BackendApiReportBody, BackendApiTransaction, BodyValue, BodyValueObject, FormData } from 'Types/SwaggerTypes'
import type { ExecuteData } from 'swagger-client/es/execute'
import type { SwaggerUIPlugin } from 'swagger-ui'

const getApiSpecUrl = (baseUrl: string, specPath: string): string => {
return `${baseUrl.replace(/\/$/, '')}${specPath}`
}

const appendSwaggerDiv = (container: HTMLElement, id: string): void => {
const div = document.createElement('div')
div.setAttribute('class', 'api-docs-wrap')
div.setAttribute('id', id)

container.appendChild(div)
}

/**
* A recursive function that traverses the tree of the multi-level object `data`
* and for every leaf (i.e. value of primitive type) adds the value to `formData` single-level object,
* with the key that is the `path` to that leaf, e.g. 'paramName[nestedArray][0][arrayProp]'
* @param formData single-level object used as accumulator
* @param data current node of the object
* @param parentKey part of the formData key inherited from the upper level
*/
const buildFormData = (formData: FormData, data: BodyValue, parentKey?: string) => {
if (data && typeof data === 'object') {
const dataObject = data as BodyValueObject
Object.keys(dataObject).forEach((key: string) => {
buildFormData(formData, dataObject[key], parentKey ? `${parentKey}[${key}]` : key)
})
} else {
if (parentKey) {
formData[parentKey] = data ? data : ''
}
}
}

/**
* Transforms an object into form data representation. Does not URL-encode, because it will be done by
* swagger-client itself
* Returns an empty object if the argument is not an object
* Example:
* {
* a_string: 'hello',
* an_array: [
* { first: 1 },
* { second: 1, extra_param: 'with whitespace'}
* ]
* }
* =>
* {
* a_string: 'hello',
* 'an_array[0][first]': '1',
* 'an_array[1][second]': '1',
* 'an_array[1][extra_param]': 'with whitespace'
* }
* @param object
*/
export const objectToFormData = (object: BodyValueObject): FormData => {
if (typeof object !== 'object' || Array.isArray(object)) {
return {}
}
const formData: FormData = {}
buildFormData(formData, object)
return formData
}

/**
* Transforms the request body of the Service Management API Report, as
* SwaggerUI (or rather swagger-js) does not serialize arrays of objects properly
* The hack is to process the request body in two steps:
* 1. normalize the 'transactions' array, as its elements may be either an object (if the value is taken
* from the example in the spec), or as a serialized JSON (if the field is changed manually)
* 2. "flatten" the objects by transforming them into form-data structure with the entries like
* 'transactions[0][app_id]': 'example'
* 'transactions[0][usage][hits]': 1
* @param body BackendApiReportBody
*/
export const transformReportRequestBody = (body: BackendApiReportBody): FormData => {
if (Array.isArray(body.transactions)) {
body.transactions = body.transactions.reduce((acc: BackendApiTransaction[], transaction) => {
let value = undefined
if (typeof transaction === 'object') {
value = transaction
} else {
value = safeFromJsonString<BackendApiTransaction>(transaction)
}
if (value) {
acc.push(value)
}
return acc
}, [])
}
return objectToFormData(body)
}

const RequestBodyTransformerPlugin: SwaggerUIPlugin = () => {
return {
fn: {
execute: (req: ExecuteData): unknown => {
if (req.contextUrl.includes('api_docs/services/service_management_api.json')
&& req.operationId === 'report'
&& req.requestBody) {
req.requestBody = transformReportRequestBody(req.requestBody as BackendApiReportBody)
}

return execute(req)
}
}
}
}

export const renderSwaggerUI = async (container: HTMLElement, apiDocsPath: string, baseUrl: string, accountDataUrl: string): Promise<void> => {
const apiSpecs: ApiDocsServices = await fetchData<ApiDocsServices>(apiDocsPath)

const accountData: AccountDataResponse = await fetchData<AccountDataResponse>(accountDataUrl)

apiSpecs.apis.forEach( api => {
const domId = api.system_name.replace(/_/g, '-')
const url = getApiSpecUrl(baseUrl, api.path)
appendSwaggerDiv(container, domId)
SwaggerUI({
url,
// eslint-disable-next-line @typescript-eslint/naming-convention -- Swagger UI
dom_id: `#${domId}`,
requestInterceptor: (request) => autocompleteRequestInterceptor(request, accountData, ''),
tryItOutEnabled: true,
plugins: [
RequestBodyTransformerPlugin
]
})
})
}
Loading