fix: fix storybook coverage task (#5256)
- Fixes storybook coverage command: the coverage directory path was incorrect, but instead of failing `storybook:test --configuration=ci`, it was hanging indefinitely. - Switches back to `concurrently` to launch `storybook:static` and `storybook:test` in parallel, which allows to use options to explicitly kill `storybook:static` when `storybook:test` fails. - Moves `storybook:test --configuration=ci` to its own command `storybook:static:test`: used in the CI, and can be used locally to run storybook tests without having to launch `storybook:dev` first. - Creates command `storybook:coverage` and enables cache for this command. - Fixes Jest tests that were failing. - Improves caching conditions for some tasks (for instance, no need to invalidate Jest test cache if only Storybook story files were modified).
This commit is contained in:
@ -24,6 +24,8 @@ runs:
|
|||||||
path: |
|
path: |
|
||||||
.cache
|
.cache
|
||||||
.nx/cache
|
.nx/cache
|
||||||
|
node_modules/.cache
|
||||||
|
packages/*/node_modules/.cache
|
||||||
key: tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }}
|
key: tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-
|
tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-
|
||||||
2
.github/workflows/ci-front.yaml
vendored
2
.github/workflows/ci-front.yaml
vendored
@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Front / Write .env
|
- name: Front / Write .env
|
||||||
run: npx nx reset:env twenty-front
|
run: npx nx reset:env twenty-front
|
||||||
- name: Run storybook tests
|
- name: Run storybook tests
|
||||||
run: npx nx storybook:test twenty-front --configuration=ci --scope=${{ matrix.storybook_scope }}
|
run: npx nx storybook:static:test twenty-front --configuration=${{ matrix.storybook_scope }}
|
||||||
front-chromatic-deployment:
|
front-chromatic-deployment:
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
|
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
|
||||||
needs: front-sb-build
|
needs: front-sb-build
|
||||||
|
|||||||
100
nx.json
100
nx.json
@ -1,11 +1,33 @@
|
|||||||
{
|
{
|
||||||
"namedInputs": {
|
"namedInputs": {
|
||||||
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
"default": ["{projectRoot}/**/*"],
|
||||||
"sharedGlobals": ["{workspaceRoot}/.github/workflows/**/*"]
|
"excludeStories": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/.storybook/*",
|
||||||
|
"!{projectRoot}/**/tsconfig.storybook.json",
|
||||||
|
"!{projectRoot}/**/*.stories.(ts|tsx)",
|
||||||
|
"!{projectRoot}/**/__stories__/*"
|
||||||
|
],
|
||||||
|
"excludeTests": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/**/jest.config.(js|ts)",
|
||||||
|
"!{projectRoot}/**/tsconfig.spec.json",
|
||||||
|
"!{projectRoot}/**/*.test.(ts|tsx)",
|
||||||
|
"!{projectRoot}/**/*.spec.(ts|tsx)",
|
||||||
|
"!{projectRoot}/**/__tests__/*"
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
"default",
|
||||||
|
"excludeStories",
|
||||||
|
"excludeTests",
|
||||||
|
"!{projectRoot}/**/__mocks__/*",
|
||||||
|
"!{projectRoot}/**/testing/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"targetDefaults": {
|
"targetDefaults": {
|
||||||
"build": {
|
"build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
|
"inputs": ["^production", "production"],
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
@ -58,6 +80,11 @@
|
|||||||
"executor": "@nx/jest:jest",
|
"executor": "@nx/jest:jest",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": [
|
||||||
|
"^default",
|
||||||
|
"excludeStories",
|
||||||
|
"{workspaceRoot}/jest.preset.js"
|
||||||
|
],
|
||||||
"outputs": ["{projectRoot}/coverage"],
|
"outputs": ["{projectRoot}/coverage"],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "{projectRoot}/jest.config.ts",
|
"jestConfig": "{projectRoot}/jest.config.ts",
|
||||||
@ -82,12 +109,7 @@
|
|||||||
"executor": "@nx/storybook:build",
|
"executor": "@nx/storybook:build",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"inputs": [
|
"inputs": ["^default", "excludeTests"],
|
||||||
"default",
|
|
||||||
"^default",
|
|
||||||
"{projectRoot}/.storybook/**/*",
|
|
||||||
"{projectRoot}/tsconfig.storybook.json"
|
|
||||||
],
|
|
||||||
"outputs": ["{options.outputDir}"],
|
"outputs": ["{options.outputDir}"],
|
||||||
"options": {
|
"options": {
|
||||||
"outputDir": "{projectRoot}/storybook-static",
|
"outputDir": "{projectRoot}/storybook-static",
|
||||||
@ -106,37 +128,51 @@
|
|||||||
"executor": "@nx/web:file-server",
|
"executor": "@nx/web:file-server",
|
||||||
"options": {
|
"options": {
|
||||||
"staticFilePath": "{projectRoot}/storybook-static",
|
"staticFilePath": "{projectRoot}/storybook-static",
|
||||||
|
"parallel": false,
|
||||||
"watch": false
|
"watch": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"storybook:coverage": {
|
||||||
|
"executor": "nx:run-commands",
|
||||||
|
"cache": true,
|
||||||
|
"inputs": [
|
||||||
|
"^default",
|
||||||
|
"excludeTests",
|
||||||
|
"{projectRoot}/coverage/storybook/coverage-storybook.json"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/coverage/storybook",
|
||||||
|
"!{projectRoot}/coverage/storybook/coverage-storybook.json"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage --cwd={projectRoot}",
|
||||||
|
"coverageDir": "coverage/storybook",
|
||||||
|
"reporter": "lcov"
|
||||||
|
}
|
||||||
|
},
|
||||||
"storybook:test": {
|
"storybook:test": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
|
"cache": true,
|
||||||
|
"inputs": ["^default", "excludeTests"],
|
||||||
|
"outputs": ["{projectRoot}/coverage/storybook"],
|
||||||
"options": {
|
"options": {
|
||||||
|
"cwd": "{projectRoot}",
|
||||||
"commands": [
|
"commands": [
|
||||||
"test-storybook -c {args.configDir} --url {args.url} --maxWorkers=3 --coverage",
|
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir}",
|
||||||
"nyc report --reporter=lcov --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage"
|
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir}"
|
||||||
],
|
],
|
||||||
"parallel": false,
|
"parallel": false,
|
||||||
"configDir": "{projectRoot}/.storybook",
|
"coverageDir": "coverage/storybook",
|
||||||
"coverageDir": "{projectRoot}/coverage/storybook",
|
"port": 6006
|
||||||
"url": "http://localhost:6006",
|
}
|
||||||
|
},
|
||||||
|
"storybook:static:test": {
|
||||||
|
"executor": "nx:run-commands",
|
||||||
|
"options": {
|
||||||
|
"commands": [
|
||||||
|
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port}'"
|
||||||
|
],
|
||||||
"port": 6006
|
"port": 6006
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"prefix": "[SB]",
|
|
||||||
"command": "nx storybook:static {projectName} --port={args.port}",
|
|
||||||
"forwardAllArgs": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "npx wait-on tcp:{args.port} && nx storybook:test {projectName}",
|
|
||||||
"forwardAllArgs": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parallel": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chromatic": {
|
"chromatic": {
|
||||||
@ -154,7 +190,11 @@
|
|||||||
},
|
},
|
||||||
"@nx/jest:jest": {
|
"@nx/jest:jest": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"inputs": ["default", "^default", "{workspaceRoot}/jest.preset.js"],
|
"inputs": [
|
||||||
|
"^default",
|
||||||
|
"excludeStories",
|
||||||
|
"{workspaceRoot}/jest.preset.js"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"passWithNoTests": true
|
"passWithNoTests": true
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const globalCoverage = {
|
const globalCoverage = {
|
||||||
|
branches: 45,
|
||||||
statements: 60,
|
statements: 60,
|
||||||
lines: 60,
|
lines: 60,
|
||||||
functions: 60,
|
functions: 60,
|
||||||
@ -6,6 +7,7 @@ const globalCoverage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const modulesCoverage = {
|
const modulesCoverage = {
|
||||||
|
branches: 45,
|
||||||
statements: 70,
|
statements: 70,
|
||||||
lines: 70,
|
lines: 70,
|
||||||
functions: 65,
|
functions: 65,
|
||||||
@ -14,6 +16,7 @@ const modulesCoverage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pagesCoverage = {
|
const pagesCoverage = {
|
||||||
|
branches: 45,
|
||||||
statements: 60,
|
statements: 60,
|
||||||
lines: 60,
|
lines: 60,
|
||||||
functions: 45,
|
functions: 45,
|
||||||
|
|||||||
@ -63,13 +63,12 @@
|
|||||||
"storybook:build:scope": {
|
"storybook:build:scope": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"command": "STORYBOOK_SCOPE={args.scope} nx storybook:build twenty-front",
|
"command": "nx storybook:build twenty-front"
|
||||||
"scope": "all"
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"docs": { "scope": "ui-docs" },
|
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||||
"modules": { "scope": "modules" },
|
"modules": { "env": { "STORYBOOK_SCOPE": "modules" } },
|
||||||
"pages": { "scope": "pages" }
|
"pages": { "env": { "STORYBOOK_SCOPE": "pages" } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storybook:dev": {
|
"storybook:dev": {
|
||||||
@ -78,13 +77,12 @@
|
|||||||
"storybook:dev:scope": {
|
"storybook:dev:scope": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"command": "STORYBOOK_SCOPE={args.scope} nx storybook:dev twenty-front",
|
"command": "nx storybook:dev twenty-front"
|
||||||
"scope": "all"
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"docs": { "scope": "ui-docs" },
|
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||||
"modules": { "scope": "modules" },
|
"modules": { "env": { "STORYBOOK_SCOPE": "modules" } },
|
||||||
"pages": { "scope": "pages" }
|
"pages": { "env": { "STORYBOOK_SCOPE": "pages" } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storybook:static": {
|
"storybook:static": {
|
||||||
@ -96,35 +94,39 @@
|
|||||||
"storybook:static:scope": {
|
"storybook:static:scope": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"command": "STORYBOOK_SCOPE={args.scope} nx storybook:static twenty-front",
|
"command": "nx storybook:static twenty-front"
|
||||||
"scope": "all"
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"docs": { "scope": "ui-docs" },
|
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||||
"modules": { "scope": "modules" },
|
"modules": { "env": { "STORYBOOK_SCOPE": "modules" } },
|
||||||
"pages": { "scope": "pages" }
|
"pages": { "env": { "STORYBOOK_SCOPE": "pages" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storybook:coverage": {
|
||||||
|
"configurations": {
|
||||||
|
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||||
|
"modules": { "env": { "STORYBOOK_SCOPE": "modules" } },
|
||||||
|
"pages": { "env": { "STORYBOOK_SCOPE": "pages" } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storybook:test": {
|
"storybook:test": {
|
||||||
"options": {
|
"options": {
|
||||||
"url": "http://localhost:6006",
|
|
||||||
"port": 6006
|
"port": 6006
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"ci": {
|
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||||
"commands": [
|
"modules": { "env": { "STORYBOOK_SCOPE": "modules" } },
|
||||||
{
|
"pages": { "env": { "STORYBOOK_SCOPE": "pages" } }
|
||||||
"prefix": "[SB]",
|
}
|
||||||
"command": "nx storybook:static {projectName} --port={args.port}",
|
},
|
||||||
"forwardAllArgs": false
|
"storybook:static:test": {
|
||||||
},
|
"options": {
|
||||||
{
|
"commands": [
|
||||||
"command": "STORYBOOK_SCOPE={args.scope} npx wait-on tcp:{args.port} && nx storybook:test {projectName}",
|
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
|
||||||
"forwardAllArgs": false
|
],
|
||||||
}
|
"port": 6006
|
||||||
],
|
},
|
||||||
"parallel": true
|
"configurations": {
|
||||||
},
|
|
||||||
"docs": { "scope": "ui-docs" },
|
"docs": { "scope": "ui-docs" },
|
||||||
"modules": { "scope": "modules" },
|
"modules": { "scope": "modules" },
|
||||||
"pages": { "scope": "pages" }
|
"pages": { "scope": "pages" }
|
||||||
|
|||||||
@ -62,8 +62,8 @@ describe('generateCsv', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const csv = generateCsv({ columns, rows });
|
const csv = generateCsv({ columns, rows });
|
||||||
expect(csv).toEqual(`Foo,Empty,Nested Foo,Nested Nested
|
expect(csv).toEqual(`Foo,Empty,Nested Foo,Nested Nested,Relation
|
||||||
some field,,foo,nested`);
|
some field,,foo,nested,a relation`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -36,14 +36,16 @@ export const generateCsv: GenerateExport = ({
|
|||||||
}: GenerateExportOptions): string => {
|
}: GenerateExportOptions): string => {
|
||||||
const columnsToExport = columns.filter(
|
const columnsToExport = columns.filter(
|
||||||
(col) =>
|
(col) =>
|
||||||
!('relationType' in col.metadata && col.metadata.relationType) ||
|
!('relationType' in col.metadata) ||
|
||||||
col.metadata.relationType === 'TO_ONE_OBJECT',
|
col.metadata.relationType === 'TO_ONE_OBJECT',
|
||||||
);
|
);
|
||||||
|
|
||||||
const keys = columnsToExport.flatMap((col) => {
|
const keys = columnsToExport.flatMap((col) => {
|
||||||
const column = {
|
const column = {
|
||||||
field: `${col.metadata.fieldName}${col.type === 'RELATION' ? 'Id' : ''}`,
|
field: `${col.metadata.fieldName}${col.type === 'RELATION' ? 'Id' : ''}`,
|
||||||
title: `${col.label} ${col.type === 'RELATION' ? 'Id' : ''}`,
|
title: [col.label, col.type === 'RELATION' ? 'Id' : null]
|
||||||
|
.filter(isDefined)
|
||||||
|
.join(' '),
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldsWithSubFields = rows.find((row) => {
|
const fieldsWithSubFields = rows.find((row) => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Section } from '@react-email/components';
|
import { Section } from '@react-email/components';
|
||||||
|
|
||||||
import { useDeleteOneDatabaseConnection } from '@/databases/hooks/useDeleteOneDatabaseConnection';
|
import { useDeleteOneDatabaseConnection } from '@/databases/hooks/useDeleteOneDatabaseConnection';
|
||||||
|
import { SettingsIntegrationDatabaseConnectionSummaryCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard';
|
||||||
import { SettingsIntegrationDatabaseTablesListCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard';
|
import { SettingsIntegrationDatabaseTablesListCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard';
|
||||||
import { useDatabaseConnection } from '@/settings/integrations/database-connection/hooks/useDatabaseConnection';
|
import { useDatabaseConnection } from '@/settings/integrations/database-connection/hooks/useDatabaseConnection';
|
||||||
import { getConnectionDbName } from '@/settings/integrations/utils/getConnectionDbName';
|
import { getConnectionDbName } from '@/settings/integrations/utils/getConnectionDbName';
|
||||||
@ -9,7 +10,6 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
|||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { SettingsIntegrationDatabaseConnectionSummaryCard } from '~/pages/settings/integrations/SettingsIntegrationDatabaseConnectionSummaryCard';
|
|
||||||
|
|
||||||
export const SettingsIntegrationDatabaseConnectionShowContainer = () => {
|
export const SettingsIntegrationDatabaseConnectionShowContainer = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
ObjectEdge,
|
ObjectEdge,
|
||||||
ObjectMetadataItemsQuery,
|
ObjectMetadataItemsQuery,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
@ -3201,7 +3202,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
node: {
|
node: {
|
||||||
__typename: 'field',
|
__typename: 'field',
|
||||||
id: '7ada51cb-58be-42cd-86df-16c3f2bb8b58',
|
id: '7ada51cb-58be-42cd-86df-16c3f2bb8b58',
|
||||||
type: 'TEXT',
|
type: FieldMetadataType.Phone,
|
||||||
name: 'phone',
|
name: 'phone',
|
||||||
label: 'Phone',
|
label: 'Phone',
|
||||||
description: 'Contact’s phone number',
|
description: 'Contact’s phone number',
|
||||||
|
|||||||
@ -11,11 +11,7 @@
|
|||||||
"generateBarrels": {
|
"generateBarrels": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"inputs": [
|
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.js"],
|
||||||
"{projectRoot}/src/**/*.{ts,tsx}",
|
|
||||||
"!{projectRoot}/src/**/*.(spec|test).{ts,tsx}",
|
|
||||||
"!{projectRoot}/src/**/*.stories.{ts,tsx}"
|
|
||||||
],
|
|
||||||
"outputs": ["{projectRoot}/src/index.ts", "{projectRoot}/src/*/index.ts"],
|
"outputs": ["{projectRoot}/src/index.ts", "{projectRoot}/src/*/index.ts"],
|
||||||
"options": {
|
"options": {
|
||||||
"command": "node {projectRoot}/scripts/generateBarrels.js"
|
"command": "node {projectRoot}/scripts/generateBarrels.js"
|
||||||
@ -52,10 +48,12 @@
|
|||||||
"port": 6007
|
"port": 6007
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"storybook:coverage": {},
|
||||||
"storybook:test": {
|
"storybook:test": {
|
||||||
"options": {
|
"options": { "port": 6007 }
|
||||||
"url": "http://localhost:6007"
|
},
|
||||||
}
|
"storybook:static:test": {
|
||||||
|
"options": { "port": 6007 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user