chmod nginx.sh

This commit is contained in:
Andrey Kobelev 2026-06-24 21:10:34 +05:00
commit 059fe623ef
5528 changed files with 466294 additions and 0 deletions

32
.dockerignore Normal file
View File

@ -0,0 +1,32 @@
.git
.eslintrc
.stylelintrc
jest.config.ts
README.md
tsconfig.jest.json
packages
services
technotes
.idea
.DS_Store
.vscode
node_modules
.env
dist/server
dist/i18n
dist/shared
dist/public/build
dist/run/*
!dist/run/.keep
secrets/*
!secrets/.keep
tests/artifacts

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.json,*.yaml,*.md}]
indent_size = 2

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
dist
node_modules
src/i18n/keysets
tests/dist
build/

26
.eslintrc Normal file
View File

@ -0,0 +1,26 @@
{
"extends": [
"@gravity-ui/eslint-config",
"@gravity-ui/eslint-config/prettier",
"@gravity-ui/eslint-config/import-order"
],
"root": true,
"env": {
"node": true,
"jest": true
},
"rules": {
"@typescript-eslint/consistent-type-imports": "error",
"security/detect-child-process": 0,
"security/detect-non-literal-fs-filename": 0,
"camelcase": 0,
"no-param-reassign": [
"warn",
{
"props": true,
"ignorePropertyModificationsFor": ["acc"]
}
],
"no-console": ["error", {"allow": ["warn", "error"]}]
}
}

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
.idea
.DS_Store
.vscode
.history
node_modules
.env
.build
dist/server/*
dist/ui/*
dist/i18n
dist/shared
dist/public/build
dist/run/*
build/
secrets/*
!secrets/.keep
tests/dist/*
src/i18n/keysets
tests/metadata
tests/certs
tests/test-results
tests/artifacts
tests/logs
reference.json
input.json
result.json
.last-run.json

14
.lintstagedrc.json Normal file
View File

@ -0,0 +1,14 @@
{
"!(src/ui/**/*)*.{js,jsx,ts,tsx}": [
"eslint --fix --quiet",
"prettier --write"
],
"src/ui/**/*.{js,jsx,ts,tsx}": [
"eslint -c src/ui/.ci-eslintrc --fix --quiet",
"prettier --write"
],
"*.{css,scss}": [
"stylelint --fix --quiet",
"prettier --write"
]
}

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
unsafe-perm=true
registry=https://registry.npmjs.org
frozen-lockfile=true

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
src/i18n/keysets
src/i18n-keysets
tests/dist
tests/artifacts
*.yaml
build/

1
.prettierrc.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('@gravity-ui/prettier-config');

6
.stylelintrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": ["@gravity-ui/stylelint-config", "@gravity-ui/stylelint-config/prettier"],
"rules": {
"declaration-block-no-redundant-longhand-properties": null
}
}

1
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1 @@
We welcome everyone to contribute to our product, see [CONTRIBUTING.md](CONTRIBUTING.md).

33
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,33 @@
# Notice to external contributors
## General info
Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Yandex Contributor License Agreement (the "**CLA**"). The current version of the CLA can be found here https://yandex.ru/legal/cla/?lang=en
By adopting the CLA, you state the following:
- You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA,
- You have read the terms and conditions of the CLA and agree with them in full,
- You are legally able to provide and license your contributions as stated,
- We may use your contributions for our open source projects and for any other our project too,
- We rely on your assurances concerning the rights of third parties in relation to your contributions.
If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have already read and adopt our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA.
## Provide contributions
If you have already adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it:
```
I hereby agree to the terms of the CLA available at: [link].
```
Replace the bracketed text as follows:
- [link] is the link to the current version of the CLA: https://yandex.ru/legal/cla/?lang=en.
It is enough to provide us such notification once.
## Other questions
If you have any questions, please mail us at datalens-opensource@yandex-team.ru.

134
Dockerfile Normal file
View File

@ -0,0 +1,134 @@
ARG UBUNTU_VERSION=24.04
# use native build platform for build js files only once
FROM --platform=${BUILDPLATFORM} ubuntu:${UBUNTU_VERSION} AS native-build-stage
ARG BUILDARCH
ARG NODE_MAJOR=22
ARG PNPM_VERSION=10.17.1
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y upgrade && apt-get -y install ca-certificates curl gnupg
# node
RUN mkdir -p /etc/apt/keyrings && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get -y install nodejs g++ make
RUN npm install -g pnpm@${PNPM_VERSION}
RUN useradd -m -u 1001 app && mkdir /opt/app && chown app:app /opt/app
WORKDIR /opt/app
COPY package.json pnpm-lock.yaml .npmrc /opt/app/
RUN --mount=type=cache,id=pnpm-store-${BUILDARCH},target=/pnpm_store \
pnpm config set store-dir=/pnpm_store --location=project && \
pnpm config set cache-dir=/pnpm_cache --location=project && \
pnpm config delete virtual-store-dir --location=project && \
pnpm install --frozen-lockfile --prefer-offline
COPY ./dist /opt/app/dist
COPY ./src /opt/app/src
COPY app-builder.config.ts tsconfig.json /opt/app/
RUN pnpm run build && chown app /opt/app/dist/run
# runtime base image for both platform
FROM ubuntu:${UBUNTU_VERSION} AS base-stage
ARG NODE_MAJOR=22
ARG PNPM_VERSION=10.17.1
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y upgrade && apt-get -y install ca-certificates curl gnupg
# node
RUN mkdir -p /etc/apt/keyrings && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get -y install nginx supervisor nodejs
RUN npm install -g pnpm@${PNPM_VERSION}
# remove unnecessary packages
RUN apt-get -y purge curl gnupg gnupg2 && \
apt-get -y autoremove && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
rm -rf /etc/apt/sources.list.d/nodesource.list && \
rm -rf /etc/apt/keyrings/nodesource.gpg
# timezone setting
ENV TZ="Etc/UTC"
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# user app
RUN useradd -m -u 1001 app && mkdir /opt/app && chown app:app /opt/app
# install package dependencies for production
FROM base-stage AS install-stage
ARG TARGETARCH
# install system dependencies
RUN apt-get update && apt-get -y install g++ make
WORKDIR /opt/app
COPY package.json pnpm-lock.yaml .npmrc /opt/app/
RUN --mount=type=cache,id=pnpm-store-${TARGETARCH},target=/pnpm_store \
pnpm config set store-dir=/pnpm_store --location=project && \
pnpm config set cache-dir=/pnpm_cache --location=project && \
pnpm config delete virtual-store-dir --location=project && \
pnpm install --frozen-lockfile --prefer-offline --prod
# production running stage
FROM base-stage AS runtime-stage
COPY deploy/nginx /etc/nginx
COPY deploy/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
# prepare rootless permissions for supervisor and nginx
ARG USER=app
RUN chmod +x /etc/nginx/entrypoint.sh && \
chown -R ${USER} /etc/nginx && \
chown -R ${USER} /etc/supervisor && \
rm -rf /etc/supervisor/conf.d && \
rm -rf /etc/nginx/sites-available && \
rm -rf /etc/nginx/sites-enabled && \
rm -rf /etc/nginx/nginx-default.conf
ARG app_version
ENV APP_VERSION=$app_version
ENV TMPDIR=/tmp
WORKDIR /opt/app
COPY --from=install-stage /opt/app/package.json /opt/app/pnpm-lock.yaml /opt/app/
COPY --from=install-stage /opt/app/node_modules /opt/app/node_modules
COPY --from=native-build-stage /opt/app/dist /opt/app/dist
RUN chown -R ${USER} /opt/app/dist/run
USER app
ENV NODE_ENV=production
ENV APP_BUILDER_CDN=false
ENV UI_CORE_CDN=false
ENV APP_MODE=full
ENV APP_ENV=production
ENV APP_INSTALLATION=opensource
ENV APP_PORT=3030
EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Copyright 2023 YANDEX LLC
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 YANDEX LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

80
README.md Normal file
View File

@ -0,0 +1,80 @@
# DataLens
### Prerequisites
[Install docker](https://docs.docker.com/engine/install/)
[Install docker compose plugin](https://docs.docker.com/compose/install/linux/) if it not already installed
### Start project in dev mode
#### In Docker mode (easy-mode):
On Linux systems:
```bash
git clone git@github.com:datalens-tech/datalens.git
cd datalens
./init.sh --ipv6 --docker-ipv6 --dev-light --dev-root --dev-ui
```
On macOS systems:
```bash
git clone git@github.com:datalens-tech/datalens.git
cd datalens
./init.sh --dev-light --dev-ui
```
#### Local mode:
Install Node.js >= v18.17.0 manually or via [node version manager](https://github.com/nvm-sh/nvm).
Start project in dev mode:
```bash
# Start backend for datalens:
git clone git@github.com:datalens-tech/datalens.git
cd datalens
docker compose -f docker-compose.dev.yaml up
# Start datalens ui in dev mode:
git clone git@github.com:datalens-tech/datalens-ui.git
cd ui
pnpm install --frozen-lockfile
pnpm run dev
```
Now you can open datalens in dev mode at [http://localhost:8080](http://localhost:8080)
### Credentials for postgres
Hostname:
```
pg-demo-connection
```
Port:
```
5432
```
Path to database:
```
demo
```
Username:
```
demo
```
Password:
```
demo
```

21
SECURITY.md Normal file
View File

@ -0,0 +1,21 @@
# Security Policy
## Reporting a Vulnerability
We're extremely grateful for security researchers and users who report vulnerabilities they discovered in DataLens. All reports are thoroughly investigated.
To report a potential vulnerability in DataLens please email details to [datalens-security@yandex-team.ru](mailto:datalens-security@yandex-team.ru).
### When Should I Report a Vulnerability?
- You think you discovered a potential security vulnerability in DataLens
- You are unsure how a vulnerability affects DataLens
## Security Vulnerability Response
Each report is acknowledged and analyzed by DataLens maintainers within 5 working days.
We will keep the reporter informed about the issue progress.
## Public Disclosure Timing
A public disclosure date is negotiated by DataLens maintainers and the bug submitter. We prefer to fully disclose the bug as soon as possible once a mitigation is available for DataLens users. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for vendor coordination. The timeframe for disclosure is from immediate (especially if it's already publicly known) to 90 days. For a vulnerability with a straightforward mitigation, we expect report date to disclosure date to be on the order of 7 days.

8
api/server/app-env.ts Normal file
View File

@ -0,0 +1,8 @@
export {
appEnv,
isChartsMode,
isDatalensMode,
isFullMode,
isApiMode,
isPublicApiMode,
} from '../../src/server/app-env';

1
api/server/callbacks.ts Normal file
View File

@ -0,0 +1 @@
export {onFail, onMissingEntry, defaultOnFail} from '../../src/server/callbacks';

View File

@ -0,0 +1,9 @@
export {
initChartsEngine,
applyPluginRoutes,
} from '../../src/server/modes/charts/init-charts-engine';
export {resolveParams} from '../../src/server/components/charts-engine/components/utils';
export {getTelemetryCallbacks} from '../../src/server/modes/charts/telemetry';
export {RUNNER_NAME} from '../../src/server/components/charts-engine/runners/constants';

45
api/server/components.ts Normal file
View File

@ -0,0 +1,45 @@
export {CacheClient} from '../../src/server/components/cache-client';
export {RedisConfig, getRedisConfig} from '../../src/server/utils/redis';
export {
getLandingLayout,
Utils,
getChartkitLayoutSettings,
getPlatform,
} from '../../src/server/components';
export {getAppLayoutSettings} from '../../src/server/components/app-layout/app-layout-settings';
export {default as resolveEntryByLink} from '../../src/server/components/resolve-entry-by-link';
export {default as metrikaDataFormatter} from '../../src/server/components/metrika-data-formatter';
export {
ChartsEngine,
CommentsFetcher,
Console,
DataFetcher,
} from '../../src/server/components/charts-engine';
export {Request} from '../../src/server/components/charts-engine/components/request';
export {renderHTML} from '../../src/server/components/charts-engine/components/markdown';
export {initPublicApiSwagger} from '../../src/server/components/public-api';
export {
PUBLIC_API_ROUTE,
PUBLIC_API_VERSION,
PUBLIC_API_VERSION_HEADER,
PUBLIC_API_ACTION_REQ_PARAM,
PUBLIC_API_ACTION_NAME,
} from '../../src/server/components/public-api';
export {getPublicApiActionsV1} from '../../src/server/components/public-api/config';
export type {
PublicApiBaseConfig,
PublicApiConfig,
PublicApiSecuritySchemes,
} from '../../src/server/components/public-api/types';
export {ApiTag as PublicApiTag} from '../../src/server/components/public-api/constants';
export {
preparePublicApiBaseConfig,
parsePublicApiVersionHeader,
} from '../../src/server/components/public-api/utils';
export {createAuthArgsMiddleware} from '../../src/server/components';

4
api/server/configs.ts Normal file
View File

@ -0,0 +1,4 @@
export {default as common} from '../../src/server/configs/common';
export {default as datalensChartTemplate} from '../../src/server/configs/shared/datalens-chart-template';
export {default as qlChartTemplate} from '../../src/server/configs/shared/ql-chart-template';
export {default as controlDashChartTemplate} from '../../src/server/configs/shared/control-dash-chart-template';

3
api/server/constants.ts Normal file
View File

@ -0,0 +1,3 @@
export {SERVICE_NAME_DATALENS} from '../../src/server/constants';
export {IPV6_AXIOS_OPTIONS} from '../../src/server/constants/axios';
export {PUBLIC_API_ORG_ID_HEADER} from '../../src/server/constants/public-api';

View File

@ -0,0 +1,3 @@
export {ping} from '../../src/server/controllers/ping';
export {chartsController} from '../../src/server/components/charts-engine/controllers/charts';
export {createPublicApiController} from '../../src/server/controllers';

1
api/server/dev-env.ts Normal file
View File

@ -0,0 +1 @@
import('../../src/server/local-dev');

1
api/server/expresskit.ts Normal file
View File

@ -0,0 +1 @@
export {getExpressKit} from '../../src/server/expresskit';

1
api/server/local-dev.ts Normal file
View File

@ -0,0 +1 @@
import '../../src/server/local-dev';

10
api/server/middlewares.ts Normal file
View File

@ -0,0 +1,10 @@
export {
xDlContext,
scrRequests,
getCtxMiddleware,
getConnectorIconsMiddleware,
beforeAuthDefaults,
serverFeatureWithBoundedContext,
patchLogger,
createAppLayoutMiddleware,
} from '../../src/server/middlewares';

7
api/server/plugins.ts Normal file
View File

@ -0,0 +1,7 @@
export {configuredDashApiPlugin} from '../../src/server/modes/charts/plugins/dash-api';
export {dashApiValidation} from '../../src/server/modes/charts/plugins/data-api-json-schema';
export {plugin as ql} from '../../src/server/modes/charts/plugins/ql';
export {configurableRequestWithDatasetPlugin} from '../../src/server/modes/charts/plugins/request-with-dataset';
export {plugin as loginsBlacklist} from '../../src/server/modes/charts/plugins/logins-blacklist';
export {plugin as runnerKeyAdapter} from '../../src/server/modes/charts/plugins/runner-key-adapter';

5
api/server/registry.ts Normal file
View File

@ -0,0 +1,5 @@
export {registry} from '../../src/server/registry';
export {registerAppPlugins} from '../../src/server/registry/utils/register-app-plugins';
export {OnEmbedsControllerBeforeResponse} from '../../src/server/registry/units/common/functions-map/on-embeds-controller-before-response';
export {OnEmbedsControllerStart} from '../../src/server/registry/units/common/functions-map/on-embeds-controller-start';

16
api/server/types.ts Normal file
View File

@ -0,0 +1,16 @@
export type {
GetLayoutConfig,
AppLayoutSettings,
AppLayoutSettingsName,
} from '../../src/server/types/app-layout';
export type {
BasicControllers,
ExtendedAppRouteDescription,
} from '../../src/server/types/controllers';
export type {Plugin, SourceConfig} from '../../src/server/components/charts-engine/types';
export type {Graph} from '../../src/server/components/charts-engine/components/processor/comments-fetcher';
export type {ResolvedConfig} from '../../src/server/components/charts-engine/components/storage/types';
export type {PublicApiVersionConfig} from '../../src/server/components/public-api/types';
export type {AnyApiServiceActionConfig} from '../../src/server/types/gateway';

14
api/server/utils.ts Normal file
View File

@ -0,0 +1,14 @@
export {
getGatewayConfig,
isGatewayError,
GatewayApiErrorResponse,
} from '../../src/server/utils/gateway';
export {getUtilsAxios} from '../../src/server/utils/axios';
export {getConfiguredRoute, getDashboardsRedirectPath} from '../../src/server/utils/routes';
export {addTranslationsScript} from '../../src/server/utils/language';
export {getEnvCert} from '../../src/server/utils/env-utils';
import {default as utils} from '../../src/server/utils';
export const getFormattedLogin = utils.getFormattedLogin;
export const getEnvVariable = utils.getEnvVariable;
export const getName = utils.getName;

View File

@ -0,0 +1 @@
export {Collapse} from '../../../src/ui/components/Collapse/Collapse';

View File

@ -0,0 +1 @@
export {ViewError} from '../../../src/ui/components/ViewError/ViewError';

View File

@ -0,0 +1,2 @@
export {DL, PRODUCT_NAME, DLS_SUBJECT, URL_OPTIONS} from '../../../src/ui/constants/common';
export {SYSTEM_THEME} from '../../../src/shared';

1
api/ui/constants/yfm.ts Normal file
View File

@ -0,0 +1 @@
export {YFM_MARKDOWN_CLASSNAME} from '../../../src/ui/constants/yfm';

2
api/ui/hocs.ts Normal file
View File

@ -0,0 +1,2 @@
export {withHiddenUnmount} from '../../src/ui/hoc/withHiddenUnmount';
export {withEnabledFeature} from '../../src/ui/hoc/withEnabledFeature';

1
api/ui/hooks.ts Normal file
View File

@ -0,0 +1 @@
export {usePrevious, useEffectOnce} from '../../src/ui/hooks';

1
api/ui/libs/logger.ts Normal file
View File

@ -0,0 +1 @@
export {default as logger} from '../../../src/ui/libs/logger';

1
api/ui/libs/metrica.ts Normal file
View File

@ -0,0 +1 @@
export {CounterName, GoalId, reachMetricaGoal} from '../../../src/ui/libs/metrica';

2
api/ui/libs/oldSdk.ts Normal file
View File

@ -0,0 +1,2 @@
export type {ConfigSdk} from '../../../src/ui/libs/sdk/types';
export {default as SDK, sdk} from '../../../src/ui/libs/sdk';

5
api/ui/libs/sdk.ts Normal file
View File

@ -0,0 +1,5 @@
export {
handleRequestError,
registerSDKDispatch,
} from '../../../src/ui/libs/schematic-sdk/parse-error';
export type {OperationError, DatalensSdk} from '../../../src/ui/libs/schematic-sdk';

12
api/ui/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "../../dist",
"resolveJsonModule": true,
"jsx": "react",
"module": "ESNext",
},
"include": [
"**/*"
]
}

1
api/ui/typings.ts Normal file
View File

@ -0,0 +1 @@
export type {DataLensApiError} from '../../src/ui/typings';

1
api/ui/utils/absurd.ts Normal file
View File

@ -0,0 +1 @@
export {absurd} from '../../../src/ui/utils/absurd';

4
api/ui/utils/common.ts Normal file
View File

@ -0,0 +1,4 @@
import {default as utils} from '../../../src/ui/utils/utils';
export const getEntryNameFromKey = utils.getEntryNameFromKey;
export const parseErrorResponse = utils.parseErrorResponse;

101
app-builder.config.ts Normal file
View File

@ -0,0 +1,101 @@
import * as fs from 'fs';
import * as path from 'path';
import type {ServiceConfig} from '@gravity-ui/app-builder';
// eslint-disable-next-line import/no-extraneous-dependencies
import type {FileCacheOptions, MemoryCacheOptions} from 'webpack';
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath: string) => path.resolve(appDirectory, relativePath);
const getFileCacheConfig = () => {
if (process.env.APP_ENV === 'development') {
return {
type: 'filesystem',
} as FileCacheOptions;
} else {
return {
type: 'memory',
} as MemoryCacheOptions;
}
};
const vendors = (vendorsList: string[]) => {
return vendorsList.concat([
'react-split-pane',
'react-dnd',
'react-grid-layout',
'react-beautiful-dnd',
'@floating-ui/react',
]);
};
const devClientPort = process.env?.['DEV_CLIENT_PORT'];
const devServerPort = process.env?.['DEV_SERVER_PORT'];
const config: ServiceConfig = {
client: {
bundler: 'rspack',
alias: {
i18n: 'src/i18n',
shared: 'src/shared',
ui: 'src/ui',
},
modules: [
'node_modules',
resolveApp('node_modules'),
resolveApp('src/ui'),
resolveApp('src/ui/units'),
],
includes: ['src/shared', 'src/i18n', 'node_modules/monaco-editor/esm/vs'],
excludeFromClean: ['!i18n', '!i18n/**/*'],
vendors,
icons: ['src/ui/assets/icons', 'node_modules/@gravity-ui/icons'],
monaco: {
languages: ['typescript', 'javascript', 'json', 'sql', 'mysql'],
},
polyfill: {
process: true,
},
disableReactRefresh: true,
contextReplacement: {
locale: ['ru', 'en'],
},
watchOptions: {
ignored: '**/server',
aggregateTimeout: 1000,
},
cache: getFileCacheConfig(),
externals: {
highcharts: 'Highcharts',
},
fallback: {
url: require.resolve('url'),
'react/jsx-runtime': require.resolve('react/jsx-runtime'),
path: false,
fs: false,
'cose-base': false,
'layout-base': false,
'highlight.js': false,
buffer: false,
},
javaScriptLoader: 'swc',
...(devClientPort
? {
devServer: {
port: parseInt(devClientPort, 10),
},
}
: {}),
},
server: {
watch: ['dist/i18n', 'dist/shared'],
...(devServerPort
? {
port: parseInt(devServerPort, 10),
}
: {}),
},
};
export default config;

View File

@ -0,0 +1,30 @@
#!/bin/sh
set -e
DEFAULT_CONF_FILE="etc/nginx/nginx.conf"
# check if we have ipv6 available
if [ -f "/proc/net/if_inet6" ]; then
# check config file exists
if [ -f "/$DEFAULT_CONF_FILE" ]; then
# check write permission
if touch /$DEFAULT_CONF_FILE 2>/dev/null; then
# check if the file is already modified, e.g. on a container restart
if grep -q -s "listen \[::]\:8080;" /$DEFAULT_CONF_FILE; then
echo '{"level":"INFO","msg":"ipv6 listen already enabled"}'
else
# enable ipv6 on nginx.conf listen sockets
sed -i -E 's|listen 8080;|listen 8080;\n listen [::]:8080;|' /$DEFAULT_CONF_FILE
fi
else
echo '{"level":"INFO","msg":"can not modify /'"${DEFAULT_CONF_FILE}"' (read-only file system?)"}'
fi
else
echo '{"level":"INFO","msg":"/'"${DEFAULT_CONF_FILE}"' is not a file or does not exist"}'
fi
else
echo '{"level":"INFO","msg":"ipv6 not available"}'
fi
exec '/usr/sbin/nginx'

121
deploy/nginx/nginx.conf Normal file
View File

@ -0,0 +1,121 @@
worker_processes auto;
error_log /proc/self/fd/1 crit;
pid /tmp/nginx.pid;
daemon off;
events {
worker_connections 1024;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
map $msec $msec_no_decimal { ~(.*)\.(.*) $1$2; }
map "$http_x_forwarded_for:$remote_addr" $fallback_forwarded_for {
default "$http_x_forwarded_for, $realip_remote_addr";
"~^:" $realip_remote_addr;
}
log_format custom escape=json
'{'
'"time":$msec_no_decimal,'
'"pid":"$pid",'
'"name":"datalens-ui",'
'"hostname":"$hostname",'
'"req":{'
'"id":"$request_id",'
'"method":"$request_method",'
'"url":"$request_uri",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent",'
'"headers":{'
'"host":"$host",'
'"x-forwarded-for":"$fallback_forwarded_for"'
'}'
'},'
'"res":{'
'"statusCode": $status'
'},'
'"responseTime":"$request_time"'
'"level":"ACCESS",'
'"msg":"[$status] [$request_time] $http_host $remote_addr `$request` [$request_id]"'
'}';
access_log /proc/self/fd/1 custom;
error_log /proc/self/fd/1 crit;
keepalive_timeout 100;
proxy_connect_timeout 365s;
proxy_send_timeout 365s;
proxy_read_timeout 365s;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:128m;
ssl_session_timeout 28h;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
proxy_buffer_size 32k;
proxy_buffers 8 32k;
fastcgi_buffers 8 32k;
fastcgi_buffer_size 32k;
client_header_buffer_size 5120k;
large_client_header_buffers 16 5120k;
client_max_body_size 200m;
client_body_buffer_size 128k;
include /etc/nginx/mime.types;
default_type application/octet-stream;
gzip on;
gzip_types text/plain text/css application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json;
server {
listen 8080;
add_header Access-Control-Allow-Headers "Content-Type,X-Request-ID,X-Charts-Cache-Token,X-Charts-Request-ID,X-CSRF-Token,X-DL-Allow-Superuser,X-DL-Sudo,X-Chart-Id,X-Timezone-Offset,X-Dash-Info,x-dl-tenantid";
add_header Access-Control-Expose-Headers "X-Request-Id, X-Trace-Id" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Max-Age 86400;
if ($request_method = OPTIONS) {
return 200;
}
proxy_hide_header Access-Control-Allow-Origin;
root /opt/app/dist/public;
location / {
try_files $uri @node;
}
location @node {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:3030;
proxy_redirect off;
}
}
fastcgi_intercept_errors on;
}

View File

@ -0,0 +1,28 @@
[supervisord]
nodaemon=true
pidfile=/tmp/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
childlogdir=/tmp
[unix_http_server]
file=/tmp/supervisor.sock
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock
[program:node]
command=node dist/server
autostart=true
autorestart=unexpected
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:nginx]
command=/etc/nginx/entrypoint.sh
autostart=true
autorestart=unexpected
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true

View File

@ -0,0 +1,37 @@
server {
listen 80;
server_name localhost;
root <project_path>/dist/public/;
access_log logs/opensource-datalens.access.log;
error_log logs/log/nginx/opensource-datalens.error.log;
location / {
try_files $uri @node;
}
location /build/ {
try_files $uri @build;
}
location @node {
proxy_pass http://unix:<project_path>/dist/run/server.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Request-ID $request_id;
proxy_redirect off;
}
location @build {
proxy_pass http://unix:<project_path>/dist/run/client.sock;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}

6
dev/env/opensource/development.env vendored Normal file
View File

@ -0,0 +1,6 @@
APP_MODE=full
APP_ENV=development
APP_INSTALLATION=opensource
APP_DEV_MODE=1
US_MASTER_TOKEN=us-master-token

View File

@ -0,0 +1,3 @@
User-agent: *
Allow: /
Disallow: /*?

BIN
dist/public/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
dist/public/os-favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

2
dist/public/robots.txt vendored Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

28
jest.config.ts Normal file
View File

@ -0,0 +1,28 @@
import type {Config} from '@jest/types';
import {getJestBaseConfig} from './jest/base-config';
const jestConfig: Config.InitialOptions = {
projects: [
{
displayName: 'UI',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src/ui'],
...getJestBaseConfig({isUIProject: true}),
},
{
displayName: 'Shared',
testEnvironment: 'node',
roots: ['<rootDir>/src/shared'],
...getJestBaseConfig(),
},
{
displayName: 'Server',
testEnvironment: 'node',
roots: ['<rootDir>/src/server'],
...getJestBaseConfig(),
},
],
};
export default jestConfig;

51
jest/base-config.ts Normal file
View File

@ -0,0 +1,51 @@
import type {GlobalConfigTsJest, InitialOptionsTsJest} from 'ts-jest';
import {getIgnoredNodeModulesRegexp} from './mappers/ignore-node-modules-mapper';
import {CSS_MAPPER, TYPESCRIPT_ALIASES_MAPPER} from './mappers/moduleNameMappers';
import {UI_GLOBAL_MOCK_PATH} from './mocks';
import {
TESTING_LIBRARY_SETUP_AFTER_ENV_FILE_PATH,
TESTING_LIBRARY_SETUP_FILE_PATH,
} from './setup-files';
import {IMAGE_TRANSFORMER} from './transformers';
const tsconfig = require('../tsconfig.jest.json');
export const getJestBaseConfig = (options?: {isUIProject?: boolean}): InitialOptionsTsJest => {
const transform = {};
const globals: GlobalConfigTsJest = {
'ts-jest': {
tsconfig: 'tsconfig.jest.json',
isolatedModules: true,
},
};
const moduleNameMapper = {
...TYPESCRIPT_ALIASES_MAPPER,
};
const setupFiles: string[] = [];
const setupFilesAfterEnv: string[] = [];
const transformIgnorePatterns: string[] = [];
if (options?.isUIProject) {
Object.assign(transform, IMAGE_TRANSFORMER);
Object.assign(moduleNameMapper, CSS_MAPPER);
setupFiles.push(TESTING_LIBRARY_SETUP_FILE_PATH, UI_GLOBAL_MOCK_PATH);
setupFilesAfterEnv.push(TESTING_LIBRARY_SETUP_AFTER_ENV_FILE_PATH);
transformIgnorePatterns.push(getIgnoredNodeModulesRegexp());
}
return {
preset: 'ts-jest/presets/js-with-ts',
modulePaths: [tsconfig.compilerOptions.baseUrl],
testMatch: ['**/*.test.[jt]s?(x)'],
moduleNameMapper,
transform,
setupFiles,
setupFilesAfterEnv,
transformIgnorePatterns,
globals,
};
};

View File

@ -0,0 +1,29 @@
// Usually jest ignores node_modules, since in an ideal world all modules are already correctly assembled
// Here we add modules that need to be run through the ts-jest transformer
const IGNORE_NODE_MODULES_LIST = [
'@gravity-ui',
'@diplodoc/latex-extension',
'@diplodoc/mermaid-extension',
'react-dnd',
'dnd-core',
'monaco-editor',
'tinygesture',
'jsondiffpatch',
'd3',
'd3-array',
'internmap',
'delaunator',
'robust-predicates',
];
export const getIgnoredNodeModulesRegexp = () => {
// Create pattern that matches module names in both npm/yarn and pnpm structures:
// - npm/yarn: node_modules/@scope/package/...
// - pnpm: node_modules/.pnpm/@scope+package@version/node_modules/@scope/package/...
// We need to handle both / and + as separators (pnpm uses + instead of / in .pnpm folder)
const modulesToTransform = IGNORE_NODE_MODULES_LIST.map((module) =>
module.replace(/[@/]/g, '[@/+]'),
).join('|');
return `node_modules/(?!.*(?:${modulesToTransform}))`;
};

View File

@ -0,0 +1,13 @@
import {pathsToModuleNameMapper} from 'ts-jest';
import {UI_STYLE_MOCK_PATH} from '../mocks';
const tsconfig = require('../../tsconfig.jest.json');
export const TYPESCRIPT_ALIASES_MAPPER = pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
prefix: '<rootDir>',
});
export const CSS_MAPPER = {
'\\.(scss|css)$': UI_STYLE_MOCK_PATH,
};

View File

@ -0,0 +1,71 @@
// The window implementation in jest does not contain a matchMedia field.
// Official description in the documentation
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
/* eslint-disable */
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
HTMLCanvasElement.prototype.getContext = () => {
// return whatever getContext has to return
};
// Adding the DL object to the Window.
Object.defineProperty(window, 'DL', {
writable: true,
value: {
user: {},
requestId: '',
endpoints: {
charts: '',
},
userSettings: {},
},
});
jest.mock(`../../src/ui/utils/utils.ts`, () => {
return {
isIframe: jest.fn(),
isEnabledFeature: jest.fn(),
};
});
jest.mock(`../../src/ui/libs/schematic-sdk/index.ts`, () => {
return {
iam: {},
bi: {},
system: {},
banners: {},
};
});
jest.mock(`../../src/ui/libs/userSettings/index.ts`, () => {
return {
UserSettings: {
getInstance: () => {
return {
getSettings: () => {
return {};
},
};
},
},
};
});
jest.mock('@gravity-ui/chartkit/gravity-charts', () => {
return {
CustomShapeRenderer: {
pieCenterText: () => {},
},
};
});

3
jest/mocks/index.ts Normal file
View File

@ -0,0 +1,3 @@
const PATH_TO_MOCKS = `<rootDir>/jest/mocks`;
export const UI_GLOBAL_MOCK_PATH = `${PATH_TO_MOCKS}/globals.mock.js`;
export const UI_STYLE_MOCK_PATH = `${PATH_TO_MOCKS}/style.mock.js`;

1
jest/mocks/style.mock.js Normal file
View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -0,0 +1,3 @@
const SETUP_FILES_PATH = '<rootDir>/jest/setup-files';
export const TESTING_LIBRARY_SETUP_FILE_PATH = `${SETUP_FILES_PATH}/testing-library.setup.ts`;
export const TESTING_LIBRARY_SETUP_AFTER_ENV_FILE_PATH = `${SETUP_FILES_PATH}/testing-library.setup-after.ts`;

View File

@ -0,0 +1,2 @@
// https://github.com/testing-library/jest-dom#usage
import '@testing-library/jest-dom';

View File

@ -0,0 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import {configure} from '@testing-library/dom';
configure({testIdAttribute: 'data-qa'});

View File

@ -0,0 +1,11 @@
const path = require('path');
// https://jestjs.io/docs/code-transformation#examples
module.exports = {
process(_sourceText, sourcePath) {
return {
code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
};
},
};

View File

@ -0,0 +1,7 @@
const JEST_PATH_TO_IMAGE_TRANSFORMER = '<rootDir>/jest/transformers/image-transformer.js';
const JEST_IMAGE_TRANSFORMER_REGEXP =
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$';
export const IMAGE_TRANSFORMER = {
[JEST_IMAGE_TRANSFORMER_REGEXP]: JEST_PATH_TO_IMAGE_TRANSFORMER,
};

295
package.json Normal file
View File

@ -0,0 +1,295 @@
{
"name": "@datalens-ui/opensource",
"version": "0.0.0",
"files": [
"build",
"scripts"
],
"description": "DataLens Opensource",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"deps:global": "pnpm --version | grep -q 10.17.1 && echo 'pnpm@10.17.1 already installed' || npm i -g pnpm@10.17.1",
"i18n:prepare": "tsc src/i18n/prepare-keysets/index.ts --esModuleInterop --outDir dist/i18n && node dist/i18n/prepare-keysets/index.js",
"dev": "pnpm run i18n:prepare && SETUP_DEV_ENV_INSTALLATION=opensource node ./scripts/setup-dev-env.js && APP_BUILDER_ENTRY_FILTER=dl-main LOCAL_DEV_PORT=8080 app-builder dev",
"build": "pnpm run i18n:prepare && NODE_ENV=production NODE_OPTIONS=--max_old_space_size=4096 APP_BUILDER_CDN=false app-builder build",
"build:analyze": "pnpm run i18n:prepare && APP_BUILDER_ANALYZE_BUNDLE=statoscope UI_CORE_CDN=false APP_BUILDER_CDN=false NODE_ENV=production NODE_ENV=production NODE_OPTIONS=--max_old_space_size=4096 app-builder build",
"cdn": "pnpm run i18n:prepare && NODE_ENV=production NODE_OPTIONS=--max_old_space_size=4096 app-builder build",
"start": "NODE_ENV=production node dist/server",
"lint:js:main": "eslint --ignore-pattern 'src/ui/*' '**/*.{js,jsx,ts,tsx}' --quiet",
"lint:js:ui": "eslint -c src/ui/.ci-eslintrc 'src/ui/**/*.{js,jsx,ts,tsx}' --quiet",
"lint:styles": "stylelint 'src/ui/**/*.scss'",
"lint:prettier:src": "prettier --check 'src/**/*.{js,jsx,ts,tsx,json,css,scss}'",
"lint:prettier:tests": "prettier --check 'tests/**/*.{js,jsx,ts,tsx,json,css,scss}'",
"lint": "pnpm run lint:prettier:src && pnpm run lint:js:main && pnpm run lint:js:ui && pnpm run lint:styles && pnpm run lint:features",
"lint:fix": "pnpm run lint:js:main --fix && pnpm run lint:js:ui --fix && pnpm run lint:styles --fix",
"lint:features": "bash scripts/ci/lint-typecheck/features.sh",
"typecheck:i18n": "tsc -p src/i18n --noEmit",
"typecheck:server": "tsc -p src/server --noEmit",
"typecheck:ui": "tsc -p src/ui --noEmit",
"typecheck:tests": "cd tests && tsc --noEmit",
"typecheck": "pnpm run i18n:prepare && pnpm run typecheck:server && pnpm run typecheck:ui && pnpm run typecheck:tests",
"test:jest": "pnpm run i18n:prepare && jest",
"test:jestWatch": "pnpm run i18n:prepare && jest --watchAll",
"platform-tools": "platform-tools",
"test:install:chromium": "npx playwright install --with-deps chromium",
"test:e2e": "cd tests && npx playwright test --config=./playwright.config.ts --project=basic",
"test:e2e:opensource": "cd tests && npx playwright test --config=./playwright.config.ts --project=opensource",
"test:e2e:us-dump": "./scripts/e2e/us-dump.sh --clear-deleted --clear-e2e --clear-revisions",
"test:e2e:docker": "cd ./tests && docker compose -f ./docker-compose.e2e.yml down --volumes && docker compose -f ./docker-compose.e2e.yml up --quiet-pull --pull always --build --no-log-prefix --exit-code-from e2e e2e",
"test:e2e:docker:snapshots": "cd ./tests && docker compose -f ./docker-compose.e2e.yml down --volumes && E2E_TEST_NAME_PATTERN=${E2E_TEST_NAME_PATTERN:-@screenshot} E2E_RETRY_TIMES=0 E2E_MAX_WORKERS=1 E2E_UPDATE_SNAPSHOTS=1 docker compose -f ./docker-compose.e2e.yml up --quiet-pull --pull always --build --no-log-prefix e2e; docker compose -f ./docker-compose.e2e.yml cp e2e:/opt/app/tests/opensource-suites/__screenshots__ ./opensource-suites",
"test:e2e:docker:report": "cd ./tests && docker compose -f ./docker-compose.e2e.yml cp e2e:/opt/app/tests/artifacts ./artifacts",
"test:e2e:docker:up": "cd ./tests && docker compose -f ./docker-compose.e2e.yml up --quiet-pull --pull always --build -d ui",
"test:e2e:docker:up-no-build": "cd ./tests && docker compose -f ./docker-compose.e2e.yml up --no-build --quiet-pull --pull always -d ui",
"test:e2e:docker:up-no-ui": "cd ./tests && docker compose -f ./docker-compose.e2e.dev.yml up --pull always -d data-api control-api us auth",
"test:e2e:docker:up-no-ui-no-auth": "cd ./tests && AUTH_TYPE=NONE AUTH_ENABLED=false docker compose -f ./docker-compose.e2e.dev.yml up --pull always -d data-api control-api us",
"test:e2e:docker:up-nginx": "cd ./tests && docker compose -f ./docker-compose.e2e.nginx.yml up -d nginx",
"test:e2e:docker:down": "cd ./tests && docker compose -f ./docker-compose.e2e.yml down --volumes",
"test:e2e:docker:down-nginx": "cd ./tests && docker compose -f ./docker-compose.e2e.nginx.yml down --volumes",
"test:e2e:docker:logs": "cd ./tests && docker --log-level error compose -f ./docker-compose.e2e.yml logs --no-color",
"test:e2e:docker:logs-nginx": "cd ./tests && docker --log-level error compose -f ./docker-compose.e2e.nginx.yml logs --no-color",
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json",
"statoscope:validate": "statoscope validate --input ./dist/public/build/stats.json"
},
"repository": {
"type": "git",
"url": "git@github.com:datalens-tech/datalens-ui.git"
},
"author": "DataLens Team <https://github.com/datalens-tech>",
"license": "Apache-2.0",
"dependencies": {
"@asteasolutions/zod-to-openapi": "^8.1.0",
"@braintree/sanitize-url": "^6.0.0",
"@datalens-tech/ui-sandbox-modules": "^0.36.0",
"@datalens-tech/xlsx": "^0.20.1",
"@diplodoc/cut-extension": "^0.7.4",
"@diplodoc/file-extension": "^0.2.1",
"@diplodoc/latex-extension": "^1.1.0",
"@diplodoc/mermaid-extension": "^1.4.0",
"@diplodoc/tabs-extension": "^3.7.5",
"@diplodoc/transform": "^4.64.1",
"@gravity-ui/app-layout": "^2.1.0",
"@gravity-ui/browserslist-config": "^4.3.0",
"@gravity-ui/chartkit": "^7.44.0",
"@gravity-ui/dashkit": "^10.0.0",
"@gravity-ui/date-utils": "^2.5.6",
"@gravity-ui/expresskit": "^3.0.2",
"@gravity-ui/gateway": "^4.10.4",
"@gravity-ui/i18n": "^1.7.0",
"@gravity-ui/nodekit": "^2.10.1",
"@node-rs/crc32": "^1.7.2",
"ajv": "^8.12.0",
"axios": "^1.13.2",
"axios-retry": "^3.9.1",
"chroma-js": "^3.1.2",
"clipboard-copy": "^3.2.0",
"dotenv": "^8.2.0",
"form-data": "^4.0.0",
"hashids": "^2.2.1",
"iconv-lite": "^0.4.24",
"ioredis": "^4.28.0",
"ismobilejs": "^1.1.1",
"js-sha1": "^0.6.0",
"js-yaml": "^4.1.0",
"json-fn": "^1.1.1",
"jsondiffpatch": "^0.6.0",
"jsonwebtoken": "^9.0.0",
"katex": "0.16.21",
"lodash": "^4.17.21",
"luxon": "^1.28.0",
"markdown-it-color": "^2.1.1",
"markdown-it-emoji": "^2.0.2",
"markdown-it-ins": "^4.0.0",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-mark": "^4.0.0",
"markdown-it-sub": "^2.0.0",
"mime": "^1.6.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.34",
"node-cache": "^5.1.2",
"object-sizeof": "^2.6.5",
"p-queue": "^6.6.2",
"qs": "^6.11.2",
"querystring": "^0.2.0",
"quickjs-emscripten": "^0.31.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hotkeys-hook": "^4.5.0",
"react-window": "^1.8.9",
"request": "^2.88.2",
"request-ip": "^3.3.0",
"request-promise-native": "^1.0.9",
"set-cookie-parser": "^2.7.1",
"swagger-ui-express": "^5.0.1",
"uuid": "^9.0.1",
"workerpool": "^9.1.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@floating-ui/react": "^0.27.13",
"@gravity-ui/app-builder": "^0.38.0",
"@gravity-ui/components": "^4.12.0",
"@gravity-ui/date-components": "^3.2.3",
"@gravity-ui/eslint-config": "^3.2.0",
"@gravity-ui/icons": "^2.16.0",
"@gravity-ui/markdown-editor": "^15.26.1",
"@gravity-ui/navigation": "^3.8.0",
"@gravity-ui/prettier-config": "^1.1.0",
"@gravity-ui/react-data-table": "^2.1.1",
"@gravity-ui/sdk": "^1.5.1",
"@gravity-ui/stylelint-config": "^4.0.1",
"@gravity-ui/tsconfig": "^1.0.0",
"@gravity-ui/ui-logger": "^1.1.0",
"@gravity-ui/uikit": "^7.18.0",
"@jest/types": "^29.6.3",
"@microsoft/fetch-event-source": "^2.0.1",
"@playwright/test": "^1.48.2",
"@reduxjs/toolkit": "^1.8.3",
"@statoscope/cli": "^5.28.2",
"@statoscope/stats-validator-plugin-webpack": "^5.28.2",
"@statoscope/webpack-ui": "^5.28.2",
"@stripe/react-stripe-js": "^1.8.1",
"@stripe/stripe-js": "^1.30.0",
"@tanstack/react-table": "^8.12.0",
"@tanstack/react-virtual": "^3.8.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.4",
"@types/chroma-js": "^3.1.2",
"@types/d3-array": "3.2.2",
"@types/d3-color": "^3.1.3",
"@types/d3-interpolate": "3.0.4",
"@types/d3-scale": "4.0.9",
"@types/d3-selection": "3.0.11",
"@types/d3-shape": "3.1.8",
"@types/dompurify": "^3.0.5",
"@types/express": "^4.17.21",
"@types/gtag.js": "0.0.10",
"@types/hashids": "^1.0.32",
"@types/history": "^4.7.11",
"@types/http-proxy": "^1.17.10",
"@types/ioredis": "^4.28.8",
"@types/jest": "^29.2.5",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.168",
"@types/luxon": "^1.26.2",
"@types/markdown-it": "^13.0.7",
"@types/markdown-it-emoji": "^2.0.2",
"@types/markdown-it-link-attributes": "^3.0.4",
"@types/mime": "1.3.5",
"@types/node": "^20.16.10",
"@types/qs": "6.9.11",
"@types/rc-slider": "^8.6.6",
"@types/react": "^17.0.40",
"@types/react-dom": "^17.0.13",
"@types/react-inspector": "^4.0.2",
"@types/react-redux": "^7.1.16",
"@types/react-router": "^5.1.11",
"@types/react-router-dom": "^5.1.7",
"@types/react-window": "^1.8.5",
"@types/recompose": "^0.30.7",
"@types/redux-logger": "^3.0.9",
"@types/request": "2.48.7",
"@types/request-ip": "^0.0.41",
"@types/request-promise-native": "^1.0.21",
"@types/set-cookie-parser": "^2.4.10",
"@types/swagger-ui-express": "^4.1.8",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/uuid": "^9.0.8",
"@types/webpack-env": "^1.16.0",
"bem-cn-lite": "^4.0.0",
"blueimp-md5": "^2.19.0",
"classnames": "^2.5.1",
"cli-color": "2.0.1",
"colormap": "^2.3.2",
"copy-to-clipboard": "^3.3.3",
"core-js": "3.43.0",
"csstype": "^3.1.3",
"d3-array": "^3.2.4",
"d3-color": "^3.1.0",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"dompurify": "^3.2.4",
"eslint": "^8.56.0",
"highcharts": "^8.2.2",
"history": "^4.10.1",
"htmlparser2": "^9.1.0",
"http-proxy": "^1.18.1",
"husky": "^8.0.3",
"immutability-helper": "^3.1.1",
"jest": "^29.3.1",
"jest-circus": "^27.5.1",
"jest-environment-jsdom": "^29.3.1",
"jest-environment-node": "^29.3.1",
"jest-html-reporters": "^2.1.2",
"lint-staged": "^13.2.2",
"lowlight": "^3.1.0",
"ml-regression-polynomial": "^3.0.2",
"mockdate": "^3.0.5",
"monaco-editor": "^0.36.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"playwright-core": "1.48.2",
"postcss": "^8.4.33",
"prettier": "^3.2.5",
"prop-types": "^15.8.0",
"rc-slider": "^11.1.8",
"react-dnd": "^10.0.2",
"react-dnd-html5-backend": "^10.0.2",
"react-inspector": "^5.1.1",
"react-intersection-observer": "^9.4.0",
"react-monaco-editor": "^0.52.0",
"react-redux": "^7.2.4",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"react-split-pane": "^0.1.92",
"react-virtualized-auto-sizer": "1.0.24",
"react-waypoint": "^10.3.0",
"recompose": "npm:react-recompose@^0.33.0",
"redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"reselect": "^4.1.8",
"resize-observer-polyfill": "^1.5.1",
"robust-point-in-polygon": "^1.0.3",
"stylelint": "^15.11.0",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^3.14.0",
"typescript": "^5.4.5",
"url": "^0.11.1",
"utility-types": "^3.10.0",
"yup": "^1.6.1"
},
"pnpm": {
"overrides": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"@gravity-ui/uikit>react-virtualized-auto-sizer": "1.0.24",
"@gravity-ui/gateway>@grpc/grpc-js": "^1.13.2"
}
},
"deploy": {
"project": "stat",
"app": "dx",
"component": "datalens",
"workflow": "workflow.json"
},
"engines": {
"pnpm": "10.17.1",
"node": ">= 20",
"yarn": "Please use pnpm instead of yarn to install dependencies",
"npm": "Please use pnpm instead of npm to install dependencies"
},
"packageManager": "pnpm@10.17.1",
"browserslist": [
"extends @gravity-ui/browserslist-config"
],
"optionalDependencies": {
"fsevents": "^2.3.2"
}
}

24773
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
#!/bin/bash
set -e
ts-node --transpile-only scripts/ci/lint-typecheck/helpers/check-features.ts

View File

@ -0,0 +1,40 @@
import fs from 'fs';
import path from 'path';
import {Feature} from '../../../../src/shared';
const featuresListPath = path.resolve(
process.cwd(),
'src/server/components/features/features-list',
);
const FEATURES_LIST: any = [];
fs.readdirSync(featuresListPath).forEach((file: string) => {
if (file === 'index.ts') {
return;
}
FEATURES_LIST.push(require(path.resolve(featuresListPath, file)).default);
});
const FEATURES = FEATURES_LIST.reduce(
(acc: Record<string, boolean>, {name}: {name: string}) => {
acc[name] = true;
return acc;
},
{} as Record<string, boolean>,
);
const missedFeatures: string[] = [];
Object.values(Feature).forEach((feature) => {
if (typeof FEATURES[feature] === 'undefined') {
missedFeatures.push(feature);
console.error(`Missed config for feature: ${feature}`);
}
});
if (missedFeatures.length) {
throw new Error('Missed config for some feature, see output above');
}

View File

@ -0,0 +1,37 @@
// variables
$after: resolveInputFile();
$inputCompilation: $after.compilations.pick();
$before: resolveReferenceFile();
$referenceCompilation: $before.compilations.pick();
// helpers
$getSizeByChunks: => files.(getAssetSize($$, true)).reduce(=> size + $$, 0);
// output
{
buildTime: {
$after: $inputCompilation.time;
$before: $referenceCompilation.time;
$after,
$before,
diff: {
value: $after - $before,
formatted: { type: 'time', a: $before, b: $after } | formatDiff() + ` (${b.percentFrom(a, 2)}%)`,
}
},
initialSize: {
$after: $inputCompilation.chunks.$getSizeByChunks($inputCompilation.hash);
$before: $referenceCompilation.chunks.$getSizeByChunks($referenceCompilation.hash);
$after,
$before,
diff: {
value: $after - $before,
formatted: { type: 'size', a: $before, b: $after } | formatDiff() + ` (${b.percentFrom(a, 2)}%)`,
}
},
validation: {
$messages: resolveInputFile().compilations.[hash].(hash.validation_getItems());
$messages,
total: $messages.size()
}
}

213
scripts/e2e/us-dump.sh Normal file
View File

@ -0,0 +1,213 @@
#!/bin/bash
# exit setup
set -eo pipefail
# [-e] - immediately exit if any command has a non-zero exit status
# [-x] - all executed commands are printed to the terminal [not secure]
# [-o pipefail] - if any command in a pipeline fails, that return code will be used as the return code of the whole pipeline
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$0")")
IS_CLEAR_DELETED="false"
IS_CLEAR_E2E="false"
IS_CLEAR_REVISIONS="false"
# parse args
for _ in "$@"; do
case ${1} in
--clear-deleted)
IS_CLEAR_DELETED="true"
shift # past argument with no value
;;
--clear-e2e)
IS_CLEAR_E2E="true"
shift # past argument with no value
;;
--clear-revisions)
IS_CLEAR_REVISIONS="true"
shift # past argument with no value
;;
-*)
echo "unknown arg: ${1}"
exit 1
;;
*) ;;
esac
done
echo ""
echo "Start dump UnitedStorage entries..."
echo " - workbooks"
echo " - collections"
echo " - entries"
echo " - revisions"
echo " - links"
if [ "${IS_CLEAR_DELETED}" = "true" ]; then
echo "+ clear deleted entries automatically..."
fi
if [ "${IS_CLEAR_E2E}" = "true" ]; then
echo "+ clear e2e entries automatically..."
fi
if [ "${IS_CLEAR_REVISIONS}" = "true" ]; then
echo "+ clear not actual entries revisions automatically..."
fi
COMPOSE_FILE=$(readlink -f "${SCRIPT_DIR}/../../tests/docker-compose.e2e.yml")
DUMP_FILE=$(readlink -f "${SCRIPT_DIR}/../../tests/data/us-e2e-data.sql")
echo ""
echo "========================"
echo "BEGIN;" >"${DUMP_FILE}"
docker --log-level error compose -f "${COMPOSE_FILE}" exec \
--env "POSTGRES_DUMP_CLEAR_META=true" \
--env "POSTGRES_DUMP_SKIP_CONFLICT=false" \
-T postgres \
/init/us-dump.sh |
sed -E 's|"cypher_text": "[^"]+"|"cypher_text": "{{POSTGRES_PASSWORD}}"|' |
sed -E 's|"host": "[^"]+"|"host": "{{POSTGRES_HOST}}"|' |
sed -E 's|"port": [^,]+,|"port": {{POSTGRES_PORT}},|' |
sed -E 's|"db_name": "[^"]+"|"db_name": "{{POSTGRES_DB}}"|' |
sed -E 's|"username": "[^"]+"|"username": "{{POSTGRES_USER}}"|' \
>>"${DUMP_FILE}"
EXIT="$?"
echo "COMMIT;" >>"${DUMP_FILE}"
echo ""
echo "========================"
if [ "${IS_CLEAR_DELETED}" = "true" ]; then
echo ""
echo "Clear deleted entries..."
DUMP=$(cat "${DUMP_FILE}")
DELETED_ENTRIES=$(
echo "${DUMP}" |
{ grep ' public.entries ' || true; } |
{ grep '__trash/' || true; } |
{ grep -oE '__trash/[0-9]+_' || true; } |
sed 's|__trash/||' |
sed 's|_||' |
tr -d ' ' |
sort |
uniq
)
IFS=$'\n'
for DELETED_ENTRY in ${DELETED_ENTRIES}; do
echo " clear deleted entry: ${DELETED_ENTRY}"
DUMP=$(echo "${DUMP}" | { grep -v ", ${DELETED_ENTRY}, " || true; } | { grep -v "(${DELETED_ENTRY}, " || true; })
done
unset IFS
echo "${DUMP}" >"${DUMP_FILE}"
fi
if [ "${IS_CLEAR_E2E}" = "true" ]; then
echo ""
echo "Clear e2e entries..."
DUMP=$(cat "${DUMP_FILE}")
E2E_ENTRIES=$(
echo "${DUMP}" |
{ grep ' public.entries ' || true; } |
{ grep 'e2e-entry-' || true; } |
{ grep -oE ", '[0-9]+/" || true; } |
sed "s|, '||" |
sed 's|/||' |
tr -d ' ' |
sort |
uniq
)
IFS=$'\n'
for E2E_ENTRY in ${E2E_ENTRIES}; do
echo " clear e2e entry: ${E2E_ENTRY}"
DUMP=$(echo "${DUMP}" | { grep -v ", ${E2E_ENTRY}, " || true; } | { grep -v "(${E2E_ENTRY}, " || true; })
done
unset IFS
echo "${DUMP}" >"${DUMP_FILE}"
fi
if [ "${IS_CLEAR_REVISIONS}" = "true" ]; then
echo ""
echo "Clear not actual revisions..."
DUMP=$(cat "${DUMP_FILE}")
ENTRIES=$(
echo "${DUMP}" |
grep ' public.entries ' |
grep -oE ", '[0-9]+/" |
sed "s|, '||" |
sed 's|/||' |
tr -d ' ' |
sort |
uniq
)
IFS=$'\n'
for ENTRY in ${ENTRIES}; do
echo " entry: ${ENTRY}"
REVISIONS=$(echo "${DUMP}" | { grep " public.revisions " || true; } | { grep ", ${ENTRY}, " || true; } | sed 's|INSERT INTO public.revisions .*VALUES|VALUES|' | sed "s|''||g" | sed -E "s|VALUES \('[^']+',||")
REVISIONS_COUNT=$(echo "${REVISIONS}" | wc -l | tr -d ' ')
echo " revisions count: ${REVISIONS_COUNT}"
if [ "${REVISIONS_COUNT}" == "0" ] || [ "${REVISIONS_COUNT}" == "1" ]; then
continue
fi
echo " clear revisions..."
for REVISION in ${REVISIONS}; do
REVISION_ID=$(echo "${REVISION}" | { grep -oE ", [0-9]+," || true; } | sed 's|,||g' | tr -d ' ')
ENTRY_REVISION=$(echo "${DUMP}" | { grep ' public.entries ' || true; } | { grep ", ${REVISION_ID}, " || true; } | tr -d ' ')
if [ ! -z "${ENTRY_REVISION}" ]; then
echo " actual revision id: ${REVISION_ID}"
continue
fi
echo " clear revision id: ${REVISION_ID}"
DUMP=$(echo "${DUMP}" | { grep -v ", ${REVISION_ID}, " || true; })
done
done
unset IFS
echo "${DUMP}" >"${DUMP_FILE}"
fi
E2E_ENTRIES=$(cat "${DUMP_FILE}" | { grep "e2e-entry-" || true; })
DELETED_ENTRIES=$(cat "${DUMP_FILE}" | { grep "__trash/" || true; })
if [ -n "${E2E_ENTRIES}" ]; then
ENTRIES_KEYS=$(echo "${E2E_ENTRIES}" | sed -E "s|INSERT INTO ([^ ]+) .*'([^']*e2e-entry-[^']*)'.*|\1 - \2|" | sed 's|^| - |')
echo ""
echo "⚠️ WARNING: Found entries with 'e2e-entry-' in key:" >&2
echo "${ENTRIES_KEYS}" >&2
echo "These entries might be test entries and should be reviewed..." >&2
fi
if [ -n "${DELETED_ENTRIES}" ]; then
ENTRIES_KEYS=$(echo "${DELETED_ENTRIES}" | sed -E "s|INSERT INTO ([^ ]+) .*'([^']*__trash/[^']*)'.*|\1 - \2|" | sed 's|^| - |')
echo ""
echo "⚠️ WARNING: Found deleted entries:" >&2
echo "${ENTRIES_KEYS}" >&2
fi
# remove empty lines
sed '/^$/N;/^\n$/D' "${DUMP_FILE}" >"${DUMP_FILE}.tmp" && mv "${DUMP_FILE}.tmp" "${DUMP_FILE}"
if [ "${EXIT}" != "0" ]; then
echo ""
echo "Dump error, exit..."
exit "${EXIT}"
else
echo ""
echo "Dump done, saved at [${DUMP_FILE}]"
exit 0
fi

View File

@ -0,0 +1,129 @@
/* eslint-disable */
'use strict';
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
const https = require('https');
const http = require('http');
// hostname: 'localhost',
// port: 3000,
// path: `/`,
// headers: {
// 'Authorization': `OAuth ${OAUTH_TOKEN}`,
// 'Content-Type': 'application/json',
// },
async function httpGet({protocol, ...options}) {
return new Promise((resolve, reject) => {
const request = protocol === 'https' ? https : http;
const req = request.get(options, (res) => {
const chunks = [];
res.on('data', function (chunk) {
chunks.push(chunk);
}).on('end', function () {
let resBody = Buffer.concat(chunks).toString();
if (res.statusCode === 200 || res.statusCode === 201) {
if (res.headers['content-type'].includes('application/json')) {
resBody = JSON.parse(resBody);
}
resolve(resBody);
} else {
reject(new Error(`Status: ${res.statusCode}, ${resBody}`));
}
});
});
req.on('error', (e) => {
reject(e);
});
});
}
// hostname: 'localhost,
// port: 3000,
// path: `/`,
// headers: {
// 'Authorization': `OAuth ${OAUTH_TOKEN}`,
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(data)
async function httpPost({body, protocol, ...options}) {
return new Promise((resolve, reject) => {
const request = protocol === 'https' ? https : http;
const req = request.request(
{
method: 'POST',
...options,
},
(res) => {
const chunks = [];
res.on('data', (data) => chunks.push(data));
res.on('end', () => {
let resBody = Buffer.concat(chunks).toString();
if (res.statusCode === 200 || res.statusCode === 201) {
if (res.headers['content-type'].includes('application/json')) {
resBody = JSON.parse(resBody);
}
resolve(resBody);
} else {
reject(new Error(`Status: ${res.statusCode}, ${resBody}`));
}
});
},
);
req.on('error', (e) => {
reject(e);
});
if (body) {
req.write(body);
}
req.end();
});
}
// hostname: 'localhost',
// port: 3000,
// path: `/`,
// headers: {
// 'Authorization': `OAuth ${OAUTH_TOKEN}`,
// 'Content-Type': 'application/json',
// },
async function httpDelete({protocol, ...options}) {
return new Promise((resolve, reject) => {
const request = protocol === 'https' ? https : http;
const req = request.request(
{
method: 'DELETE',
...options,
},
(res) => {
const chunks = [];
res.on('data', (data) => chunks.push(data));
res.on('end', () => {
let resBody = Buffer.concat(chunks).toString();
if (res.statusCode === 200 || res.statusCode === 201) {
if (res.headers['content-type'].includes('application/json')) {
resBody = JSON.parse(resBody);
}
resolve(resBody);
} else {
reject(new Error(`Status: ${res.statusCode}, ${resBody}`));
}
});
},
);
req.on('error', (e) => {
reject(e);
});
req.end();
});
}
module.exports = {
httpGet,
httpPost,
httpDelete,
};

View File

@ -0,0 +1,40 @@
const fs = require('fs');
const MAINTENANCE_LOGS_DIR = process.env.MAINTENANCE_LOGS_DIR;
if (!MAINTENANCE_LOGS_DIR) {
throw new Error('MAINTENANCE_LOGS_DIR directory path should be specified');
}
function getTimeNow() {
const now = new Date().toISOString();
return now.substring(11);
}
const now = new Date().toISOString();
const stream = fs.createWriteStream(
`${MAINTENANCE_LOGS_DIR}/dc_${now.replace(/[-:]/g, '').replace(/\./g, '_')}.log`,
{
flags: 'a',
},
);
function logger({type = 'info', message}) {
switch (type) {
case 'info':
stream.write(`[INFO] - ${getTimeNow()}: `);
stream.write(message);
break;
case 'error':
stream.write(`[ERROR] - ${getTimeNow()}: `);
stream.write(String(message));
break;
default:
stream.write(`[INFO] - ${getTimeNow()}: `);
stream.write(message);
}
stream.write('\n');
}
module.exports = {
logger,
};

View File

@ -0,0 +1,7 @@
function sleep(time) {
return new Promise((res) => setTimeout(res, time));
}
module.exports = {
sleep,
};

31
scripts/setup-dev-env.js Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env node
'use strict';
const {readFileSync, writeFileSync, openSync} = require('fs');
const path = require('path');
const SECRETS_SECTION_START = '### TEMPLATE SECRETS BEGIN';
const SECRETS_SECTION_END = '### TEMPLATE SECRETS END';
const REPLACE_REGEXP = new RegExp(`^${SECRETS_SECTION_START}.*${SECRETS_SECTION_END}$`, 'ms', 's');
const installationName = process.env.SETUP_DEV_ENV_INSTALLATION;
const envName = process.env.ENV || 'development';
const templateName = `${installationName}/${envName}.env`;
const appPath = path.join(__dirname, '..');
const templateFilePath = path.join(appPath, `dev/env/${templateName}`);
const templateContent = readFileSync(templateFilePath).toString();
const secretsSection = `${SECRETS_SECTION_START}\n${templateContent}\n${SECRETS_SECTION_END}`;
let currentEnv;
try {
currentEnv = readFileSync(path.join(appPath, '.env')).toString();
} catch (__) {
openSync(path.join(appPath, '.env'), 'w');
currentEnv = `${SECRETS_SECTION_START}\n${SECRETS_SECTION_END}`;
}
writeFileSync(path.join(appPath, '.env'), currentEnv.replace(REPLACE_REGEXP, secretsSection));

0
secrets/.keep Normal file
View File

9
src/@types/chartkit-dl.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import '../shared/types/chartkit';
declare module '../shared/types' {
export interface TableCommonCell {
color?: number;
backgroundColor?: string;
type?: 'number' | 'markup';
}
}

19
src/@types/express.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import type {RenderParams} from '@gravity-ui/app-layout';
declare global {
namespace Express {
interface Request {
blackbox?: any;
tvmSelf?: any;
nonce?: string;
}
interface Response {
renderDatalensLayout: <T>(params: RenderParams<T>) => string;
}
}
}
declare module '@gravity-ui/expresskit' {
export interface AppRouteParams {
ui?: boolean;
}
}

47
src/@types/global.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
declare module '*.svg' {
const content: SVGIconSvgrData;
export default content;
}
declare module '*.png' {
const path: string;
export default path;
}
declare module '*.jpg' {
const path: string;
export default path;
}
declare module '*.jpeg' {
const path: string;
export default path;
}
declare module '*.webp' {
const path: string;
export default path;
}
declare module 'markdown-it-mark' {
import type {PluginSimple} from 'markdown-it';
const plugin: PluginSimple;
export = plugin;
}
declare module 'markdown-it-sub' {
import type {PluginSimple} from 'markdown-it';
const plugin: PluginSimple;
export = plugin;
}
declare module 'markdown-it-ins' {
import type {PluginSimple} from 'markdown-it';
const plugin: PluginSimple;
export = plugin;
}

33
src/@types/highcharts-preparers.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
import 'highcharts';
declare module 'highcharts' {
interface PointOptionsObject {
colorValue?: string;
colorGuid?: string;
}
interface SeriesOptions {
colorGuid?: string;
colorValue?: string;
legendTitle?: string;
formattedName?: string;
}
interface Point {
userOptions: {
legendTitle: string;
formattedName?: string;
};
yLabel: string;
}
interface Series {
userOptions: {
legendTitle: string;
};
}
interface Axis {
closestPointRange: number;
}
}

9
src/@types/json-fn.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module 'json-fn' {
export function stringify(
value: any,
replacer?: (this: any, key: string, value: any) => any,
space?: string | number,
): string;
export function parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
}

155
src/@types/nodekit.d.ts vendored Normal file
View File

@ -0,0 +1,155 @@
import type {Link, Meta} from '@gravity-ui/app-layout';
import type {Request, Response} from '@gravity-ui/expresskit';
import type {CtxUser} from '../server/components/auth/types/user';
import type {ChartTemplates} from '../server/components/charts-engine/components/chart-generator';
import type {SourceConfig} from '../server/components/charts-engine/types';
import type {RedisConfig} from '../server/utils/redis';
import type {AppEnvironment, LandingPageSettings} from '../shared';
import type {UserRole} from '../shared/components/auth/constants/role';
import type {FeatureConfig} from '../shared/types';
export interface SharedAppConfig {
endpoints: Endpoints;
features: FeatureConfig;
metrika: MetrikaCounter;
usMasterToken?: string;
usDynamicMasterTokenPrivateKey?: string;
regionalEnvConfig?: {allowLanguages?: string[]; defaultLang?: string; langRegion?: string};
faviconUrl: string;
links?: Link[];
meta?: Meta[];
chartkitSettings?: ChartkitGlobalSettings;
defaultColorPaletteId?: string;
serviceName: string;
// CHARTS ENGINE -- START
usEndpoint: string;
getSourcesByEnv: (appEnv: AppEnvironment) => Record<string, SourceConfig>;
sources: Record<string, SourceConfig>;
appMode?: string;
enablePreloading?: boolean;
fetchingTimeout: number;
singleFetchingTimeout: number;
runnerExecutionTimeouts?: Record<string, Record<string, number>>;
runResponseWhitelist?: string[];
allowBodyConfig: boolean;
chartsEngineConfig: {
secrets: Record<string, string>;
enableTelemetry: boolean;
flags?: Record<string, boolean>;
usEndpointPostfix: string;
dataFetcherProxiedHeaders?: string[];
maxWorkers?: number;
includeServicePlan?: boolean;
includeTenantFeatures?: true;
tvmAllowedSources?: Record<string, number>;
tvmAllowedSourcesWithoutUserTicket?: Record<string, number>;
privateApiPrefixes?: string[];
};
// CHARTS ENGINE -- FINISH
useIPV6?: boolean;
workers?: number;
requestIdHeaderName: string;
gatewayProxyHeaders: string[];
headersMap: Record<string, string>;
iamResources?: {
collection: {
roles: {
admin: string;
editor: string;
viewer: string;
limitedViewer?: string;
entryBindingCreator?: string;
limitedEntryBindingCreator?: string;
};
};
workbook: {
roles: {
admin: string;
editor: string;
viewer: string;
limitedViewer?: string;
};
};
sharedEntry: {
roles: {
admin: string;
editor: string;
viewer: string;
limitedViewer?: string;
entryBindingCreator?: string;
limitedEntryBindingCreator?: string;
};
};
};
// auth
isAuthEnabled: boolean;
authTokenPublicKey?: string;
authManageLocalUsersDisabled?: boolean;
authSignupDisabled?: boolean;
authCookieName?: string;
// sorted roles from the role with the most rights to the role with the least
orderedAuthRoles?: `${UserRole}`[];
chartTemplates: Partial<Record<keyof ChartTemplates, unknown>>;
redis: RedisConfig | null;
apiPrefix: string;
preloadList?: string[];
releaseVersion?: string;
docsUrl?: string;
}
export interface SharedAppDynamicConfig {
features?: FeatureConfig;
}
export interface SharedAppContextParams {
userId: string;
gateway: {
reqBody: Request['body'];
requestId: string;
checkRequestForDeveloperModeAccess: (args: {
ctx: AppContext;
}) => Promise<DeveloperModeCheckStatus>;
markdown: MarkdownContextAction;
resolveEntryByLink: (
args: ResolveEntryByLinkComponentArgs,
) => Promise<ResolveEntryByLinkComponentResponse>;
};
sources: {
reqBody: Request['body'];
};
getAppLayoutSettings: (req: Request, res: Response, name?: string) => AppLayoutSettings;
landingPageSettings?: LandingPageSettings;
i18n: ServerI18n;
tenantId?: string;
user?: CtxUser;
usDynamicMasterToken?: string;
isEnabledServerFeature: (feature: string) => boolean;
}
declare module '@gravity-ui/nodekit' {
export interface AppConfig extends SharedAppConfig {}
export interface AppDynamicConfig extends SharedAppDynamicConfig {}
export interface AppContextParams extends SharedAppContextParams {}
}

4
src/@types/svg.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const value: any;
export = value;
}

20
src/@types/window.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
import type moment from 'moment';
import type {compose} from 'redux';
import type {AvailableYMActions, DLGlobalData} from '../shared';
declare global {
interface Window {
sdk: unknown;
moment: moment;
DL: DLGlobalData;
ym?: (
counterId: string | number,
action: AvailableYMActions,
targetId: string,
params?: Record<string, unknown>,
) => void;
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
clipboardData: DataTransfer;
}
}

View File

@ -0,0 +1 @@
.sync

1
src/i18n-keysets/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.sync

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,14 @@
{
"button_cancel": "Cancel",
"button_save": "Save",
"label_admin-notification": "Set a temporary password and ask the user to change it in the account settings",
"label_error-incorrect-old-password": "Incorrect current password",
"label_error-passwords-not-match": "These passwords don't match",
"label_error-required-fields": "You must fill out the required fields",
"label_new-password": "New password",
"label_old-password": "Current password",
"label_password": "Password",
"label_repeat-password": "Repeat password",
"label_success-change": "Password successfully changed",
"title_change-password": "Change password"
}

View File

@ -0,0 +1,60 @@
{
"context": "",
"allowedStatuses": [
"APPROVED",
"EXPIRED",
"GENERATED",
"REQUIRES_TRANSLATION",
"TRANSLATED"
],
"status": {
"button_cancel": {
"en": "APPROVED",
"ru": "APPROVED"
},
"button_save": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_admin-notification": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_error-incorrect-old-password": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_error-passwords-not-match": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_error-required-fields": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_new-password": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_old-password": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_password": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_repeat-password": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_success-change": {
"en": "APPROVED",
"ru": "APPROVED"
},
"title_change-password": {
"en": "APPROVED",
"ru": "APPROVED"
}
}
}

View File

@ -0,0 +1,14 @@
{
"button_cancel": "Отменить",
"button_save": "Сохранить",
"label_admin-notification": "Задайте временный пароль и попросите пользователя сменить его в настройках учётной записи",
"label_error-incorrect-old-password": "Текущий пароль указан неверно",
"label_error-passwords-not-match": "Пароли не совпадают",
"label_error-required-fields": "Поле обязательно для заполнения",
"label_new-password": "Новый пароль",
"label_old-password": "Текущий пароль",
"label_password": "Пароль",
"label_repeat-password": "Новый пароль ещё раз",
"label_success-change": "Пароль успешно изменён",
"title_change-password": "Смена пароля"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,10 @@
{
"action_user-roles-change-apply": "Save",
"action_user-roles-change-cancel": "Cancel",
"label_base-role": "Base role for service access",
"label_includes-permissions-from": "Includes permissions granted by the role",
"label_roles-change-failure": "An error occurred while editing the user's access rights",
"label_roles-change-success": "Role assigned successfully",
"label_select-role": "Select role",
"title_update-role": "Setting up access rights"
}

View File

@ -0,0 +1,44 @@
{
"context": "",
"allowedStatuses": [
"APPROVED",
"EXPIRED",
"GENERATED",
"REQUIRES_TRANSLATION",
"TRANSLATED"
],
"status": {
"action_user-roles-change-apply": {
"en": "APPROVED",
"ru": "APPROVED"
},
"action_user-roles-change-cancel": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_base-role": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_includes-permissions-from": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_roles-change-failure": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_roles-change-success": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_select-role": {
"en": "APPROVED",
"ru": "APPROVED"
},
"title_update-role": {
"en": "APPROVED",
"ru": "APPROVED"
}
}
}

View File

@ -0,0 +1,10 @@
{
"action_user-roles-change-apply": "Сохранить",
"action_user-roles-change-cancel": "Отменить",
"label_base-role": "Базовая роль для доступа к сервису",
"label_includes-permissions-from": "Включает разрешения, предоставляемые ролью",
"label_roles-change-failure": "При редактировании прав доступа пользователя произошла ошибка",
"label_roles-change-success": "Роль успешно назначена",
"label_select-role": "Выберите роль",
"title_update-role": "Настройка прав доступа"
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,8 @@
{
"action_user-profile-deletion-cancel": "Cancel",
"action_user-profile-deletion-confirm": "Delete user",
"label_delete-user-confirmation": "Are you sure you want to delete the user? After deletion, it will not be able to use the DataLens service.",
"label_delete-user-failure": "An error occurred when deleting the user",
"label_delete-user-success": "User deleted successfully",
"title_delete-user": "Deleting a user"
}

View File

@ -0,0 +1,36 @@
{
"context": "",
"allowedStatuses": [
"APPROVED",
"EXPIRED",
"GENERATED",
"REQUIRES_TRANSLATION",
"TRANSLATED"
],
"status": {
"action_user-profile-deletion-cancel": {
"en": "APPROVED",
"ru": "APPROVED"
},
"action_user-profile-deletion-confirm": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_delete-user-confirmation": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_delete-user-failure": {
"en": "APPROVED",
"ru": "APPROVED"
},
"label_delete-user-success": {
"en": "APPROVED",
"ru": "APPROVED"
},
"title_delete-user": {
"en": "APPROVED",
"ru": "APPROVED"
}
}
}

Some files were not shown because too many files have changed in this diff Show More