@formixru/b2b-ui 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ services:
2
+ web:
3
+ image: gitea.optiweb.ru/public/nginx-static
4
+ user: ${DOCKER_USER}
5
+ restart: always
6
+ volumes:
7
+ - ${HOST_DEST}:/app
8
+ labels:
9
+ - consul.meta.ingress=true
10
+ - consul.tags="www"
11
+ ports:
12
+ - :80
@@ -0,0 +1,8 @@
1
+ services:
2
+ php:
3
+ image: gitea.optiweb.ru/public/nginx-static
4
+ user: ${DOCKER_USER}
5
+ ports:
6
+ - 8080:80
7
+ volumes:
8
+ - ../../../:/app
@@ -0,0 +1,237 @@
1
+ name: deploy
2
+ on:
3
+ push:
4
+ branches: ["**"]
5
+
6
+ jobs:
7
+ deploy:
8
+ runs-on: dev.optiweb
9
+ env:
10
+ NODE_CONTAINER_LABEL: "20.12-2.0.0"
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Вычисление переменных
16
+ run: |
17
+ echo "BUILD_START=$(date +%s)" >> "$GITEA_ENV"
18
+
19
+ RAW="${{ gitea.ref_name }}"
20
+
21
+ if [[ "$RAW" =~ ^v[0-9]+(\.[0-9]+)+$ ]]; then
22
+ RAW="${RAW#v}"
23
+ fi
24
+
25
+ VERSION=$(echo "$RAW" | sed 's/[^a-zA-Z0-9]/-/g')
26
+ {
27
+ echo "VERSION=$VERSION"
28
+ echo "PROJECT=${{ gitea.event.repository.name }}"
29
+ echo "DEST=${DEPLOY_ROOT}/${{ gitea.event.repository.name }}/${VERSION}"
30
+ echo "HOST_DEST=${HOST_DEPLOY_ROOT}/${{ gitea.event.repository.name }}/${VERSION}"
31
+ echo "HOST_WORK_DIR=${HOST_WORK_ROOT}${{ gitea.workspace }}"
32
+ echo "COMPOSE_DIR=${DEPLOY_ROOT}/${{ gitea.event.repository.name }}/${VERSION}/.docker/${DEPLOY_TYPE}/compose"
33
+ } >> "$GITEA_ENV"
34
+
35
+ NODE_ADDR=$(curl -sS -H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}" \
36
+ "${CONSUL_HTTP_ADDR}/v1/agent/self" | jq -r '.DebugConfig.AdvertiseAddrLAN')
37
+ if [ -z "$NODE_ADDR" ] || [ "$NODE_ADDR" = "null" ]; then
38
+ echo "::error::Не удалось получить адрес агента"
39
+ exit 1
40
+ fi
41
+ echo "NODE_ADDR=$NODE_ADDR" >> "$GITEA_ENV"
42
+
43
+ - name: Версии ПО и переменные с которыми выполняется сборка
44
+ run: |
45
+ echo 'PROJECT:' $PROJECT
46
+ echo 'VERSION:' $VERSION
47
+ echo 'DEST:' $DEST
48
+ echo 'HOST_DEST:' $HOST_DEST
49
+ echo 'HOST_WORK_DIR:' $HOST_WORK_DIR
50
+ echo 'PLACEHOLDER:' ${PLACEHOLDER}
51
+ echo 'BASE_DOMAIN:' ${BASE_DOMAIN}
52
+ echo 'DEPLOY_TYPE:' ${DEPLOY_TYPE}
53
+ echo 'COMPOSE_DIR:' ${COMPOSE_DIR}
54
+ echo 'NODE_ADDR:' ${NODE_ADDR}
55
+
56
+ - name: Запуск сборки
57
+ run: |
58
+ docker run --rm -i \
59
+ -e YARN_CACHE_FOLDER=/.yarn-cache \
60
+ -v $HOST_WORK_DIR:/app \
61
+ -v yarn-cache:/.yarn-cache \
62
+ --pull always gitea.optiweb.ru/public/node:${NODE_CONTAINER_LABEL} \
63
+ sh -c "yarn install && yarn build && yarn lint"
64
+
65
+ - name: Копирование собранных файлов
66
+ run: |
67
+ mkdir -p "$DEST"
68
+ rsync -a --info=copy,del --delete --exclude '.git' ./ "$DEST/"
69
+
70
+ - name: Запуск контейнеров
71
+ run: docker compose -f ${COMPOSE_DIR}/docker-compose.yaml -p "${PROJECT}_${VERSION}" up -d --pull always --remove-orphans
72
+
73
+ - name: Дерегистрация прежних записей версии
74
+ run: |
75
+ SVC_NAME="${VERSION}-${PROJECT}-${PLACEHOLDER}"
76
+ curl -sS -H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}" \
77
+ "${CONSUL_HTTP_ADDR}/v1/agent/services" | \
78
+ jq -r --arg name "$SVC_NAME" \
79
+ 'to_entries[] | select(.value.Service == $name) | .key' | \
80
+ while read -r sid; do
81
+ echo "Дерегистрирую прежнюю запись: $sid"
82
+ curl -sS -H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}" \
83
+ -X PUT "${CONSUL_HTTP_ADDR}/v1/agent/service/deregister/${sid}"
84
+ done
85
+
86
+ - name: Регистрация сервисов в consul
87
+ run: |
88
+ cd "${COMPOSE_DIR}"
89
+ COMPOSE="docker compose -f docker-compose.yaml -p ${PROJECT}_${VERSION}"
90
+ SVC_NAME="${VERSION}-${PROJECT}-${PLACEHOLDER}"
91
+
92
+ ALL_TAGS="" # накопитель всех тегов ingress-сервисов
93
+
94
+ for service in $($COMPOSE ps --services); do
95
+ for cid in $($COMPOSE ps -q "$service"); do
96
+ [ -z "$cid" ] && continue
97
+
98
+ tags=""; additional_meta=""; ingress=""; first=true
99
+ while IFS='=' read -r key value; do
100
+ key=$(echo "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
101
+ value=$(echo "$value" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/^"\(.*\)"$/\1/')
102
+ case "$key" in
103
+ consul.meta.ingress)
104
+ ingress="$value" ;;
105
+ consul.meta.*)
106
+ mkey=$(echo "$key" | sed 's/^consul\.meta\.//')
107
+ $first && first=false || additional_meta="${additional_meta},"
108
+ additional_meta="${additional_meta}\"$mkey\":\"$value\"" ;;
109
+ consul.tags)
110
+ tags=$(echo "$value" | sed 's/[^,]*/"&"/g') ;;
111
+ esac
112
+ done < <(docker inspect --format='{{range $k,$v := .Config.Labels}}{{$k}}={{$v}}{{"\n"}}{{end}}' "$cid" | grep '^consul')
113
+
114
+ if [ -z "$tags" ]; then
115
+ echo "$service: без consul.tags, пропуск"; continue
116
+ fi
117
+ if [ "$ingress" != "true" ]; then
118
+ echo "$service: ingress не true (='$ingress'), пропуск роутинга"; continue
119
+ fi
120
+
121
+ pnum=$(docker inspect --format='{{range $p,$c := .NetworkSettings.Ports}}{{$p}} {{end}}' "$cid" | awk '{print $1}' | cut -d/ -f1)
122
+ [ -z "$pnum" ] && { echo "$service: нет портов, пропуск"; continue; }
123
+ pext=$($COMPOSE port "$service" "$pnum" | cut -d: -f2)
124
+ [ -z "$pext" ] && { echo "$service: порт не опубликован, пропуск"; continue; }
125
+
126
+ # накапливаем теги этого сервиса (снимаем кавычки, что sed навесил для JSON)
127
+ plain_tags=$(echo "$tags" | sed 's/"//g; s/,/ /g')
128
+ ALL_TAGS="${ALL_TAGS} ${plain_tags}"
129
+
130
+ meta="\"project\":\"${PROJECT}\",\"version\":\"${VERSION}\",\"placeholder\":\"${PLACEHOLDER}\",\"ingress\":\"${ingress}\""
131
+ [ -n "$additional_meta" ] && meta="${meta},${additional_meta}"
132
+
133
+ BODY="{
134
+ \"ID\": \"${cid}\",
135
+ \"Name\": \"${SVC_NAME}\",
136
+ \"Address\": \"${NODE_ADDR}\",
137
+ \"Port\": ${pext},
138
+ \"Tags\": [${tags}],
139
+ \"Meta\": {${meta}},
140
+ \"Check\": {
141
+ \"TCP\": \"127.0.0.1:${pext}\",
142
+ \"Interval\": \"10s\"
143
+ }
144
+ }"
145
+
146
+ echo "Регистрирую $service cid=${cid:0:12} порт=$pext теги=[$tags] ingress=$ingress"
147
+ RESP=$(curl -sS -w "\n%{http_code}" \
148
+ -H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}" \
149
+ -H 'Content-Type: application/json' \
150
+ -X PUT --data "$BODY" \
151
+ "${CONSUL_HTTP_ADDR}/v1/agent/service/register")
152
+ CODE=$(echo "$RESP" | tail -n1)
153
+ if [ "$CODE" != "200" ]; then
154
+ echo "::warning::Регистрация $service вернула $CODE: $(echo "$RESP" | head -n-1)"
155
+ fi
156
+ done
157
+ done
158
+
159
+ # нормализуем список тегов (убираем лишние пробелы) и сохраняем для шага уведомления
160
+ ALL_TAGS=$(echo "$ALL_TAGS" | tr -s ' ' | sed 's/^ //; s/ $//')
161
+ echo "ALL_TAGS=$ALL_TAGS" >> "$GITEA_ENV"
162
+ echo "Собранные теги для уведомления: $ALL_TAGS"
163
+
164
+ - name: Уведомление в Telegram
165
+ if: always()
166
+ run: |
167
+ STATUS="${{ job.status }}"
168
+ case "$STATUS" in
169
+ success) ICON="✅"; WORD="успешно" ;;
170
+ failure) ICON="❌"; WORD="с ошибкой" ;;
171
+ *) ICON="⚠️"; WORD="$STATUS" ;;
172
+ esac
173
+
174
+ # длительность сборки
175
+ if [ -n "$BUILD_START" ]; then
176
+ ELAPSED=$(( $(date +%s) - BUILD_START ))
177
+ MIN=$(( ELAPSED / 60 ))
178
+ SEC=$(( ELAPSED % 60 ))
179
+ if [ "$MIN" -gt 0 ]; then
180
+ DURATION="${MIN} м ${SEC} с"
181
+ else
182
+ DURATION="${SEC} с"
183
+ fi
184
+ else
185
+ DURATION="—"
186
+ fi
187
+
188
+ md_escape() {
189
+ printf '%s' "$1" | sed 's/[\\_*`\[]/\\&/g'
190
+ }
191
+
192
+ COMMIT_MSG_RAW=$(git log -1 --pretty=%s 2>/dev/null || echo "—")
193
+ COMMIT_MSG=$(md_escape "$COMMIT_MSG_RAW")
194
+ REF=$(md_escape "${{ gitea.ref_name }}")
195
+ ACTOR=$(md_escape "${{ gitea.actor }}")
196
+ PROJECT_ESC=$(md_escape "$PROJECT")
197
+
198
+ SHA_FULL="${{ gitea.sha }}"
199
+ SHA_SHORT="${SHA_FULL:0:8}"
200
+ COMMIT_URL="${{ gitea.server_url }}/${{ gitea.repository }}/commit/${SHA_FULL}"
201
+
202
+ DOMAINS=""
203
+ for tag in $ALL_TAGS; do
204
+ DOMAINS="${DOMAINS}\nhttp://${tag}.${VERSION}.${PROJECT}.${PLACEHOLDER}.${BASE_DOMAIN}"
205
+ done
206
+ [ -z "$ALL_TAGS" ] && DOMAINS="\n(нет ingress-сервисов)"
207
+
208
+ MESSAGE=$(printf '%s Деплой %s / %s завершён %s\nВетка: %s\nКоммит: [%s](%s)\nСообщение: %s\nАвтор: %s\nВремя сборки: %s\nПлощадка: %s.%s\nАдреса:%b' \
209
+ "$ICON" "$PROJECT_ESC" "$VERSION" "$WORD" \
210
+ "$REF" "$SHA_SHORT" "$COMMIT_URL" "$COMMIT_MSG" "$ACTOR" \
211
+ "$DURATION" "$PLACEHOLDER" "$BASE_DOMAIN" "$DOMAINS")
212
+
213
+ HTTP_BODY=$(curl -sS --max-time 15 \
214
+ -U "${TELEGRAM_PROXY_LOGIN}:${TELEGRAM_PROXY_PASS}" \
215
+ -x "${TELEGRAM_PROXY_ADDRESS}" \
216
+ -w "\nHTTP_CODE=%{http_code}" \
217
+ -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
218
+ --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
219
+ --data-urlencode "text=${MESSAGE}" \
220
+ --data-urlencode "parse_mode=Markdown" \
221
+ --data-urlencode "disable_web_page_preview=true" 2>&1)
222
+ RC=$?
223
+ echo "curl exit=$RC"
224
+ echo "response: $HTTP_BODY"
225
+ if [ $RC -ne 0 ]; then
226
+ echo "::warning::curl завершился с кодом $RC — $HTTP_BODY"
227
+ elif ! echo "$HTTP_BODY" | grep -q 'HTTP_CODE=200'; then
228
+ echo "::warning::Telegram API вернул не 200 — $HTTP_BODY"
229
+ else
230
+ echo "Уведомление отправлено"
231
+ fi
232
+ env:
233
+ TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
234
+ TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
235
+ TELEGRAM_PROXY_ADDRESS: ${{ secrets.TELEGRAM_PROXY_ADDRESS }}
236
+ TELEGRAM_PROXY_LOGIN: ${{ secrets.TELEGRAM_PROXY_LOGIN }}
237
+ TELEGRAM_PROXY_PASS: ${{ secrets.TELEGRAM_PROXY_PASS }}
@@ -0,0 +1,47 @@
1
+ name: teardown
2
+ on:
3
+ workflow_dispatch:
4
+
5
+ jobs:
6
+ teardown:
7
+ runs-on: optiweb
8
+ steps:
9
+ - name: Вычисление переменных
10
+ run: |
11
+ RAW="${{ gitea.ref_name }}"
12
+
13
+ if [[ "$RAW" =~ ^v[0-9]+(\.[0-9]+)+$ ]]; then
14
+ RAW="${RAW#v}"
15
+ fi
16
+
17
+ VERSION=$(echo "$RAW" | sed 's/[^a-zA-Z0-9]/-/g')
18
+ {
19
+ echo "VERSION=$VERSION"
20
+ echo "PROJECT=${{ gitea.event.repository.name }}"
21
+ echo "DEST=${DEPLOY_ROOT}/${{ gitea.event.repository.name }}/${VERSION}"
22
+ echo "HOST_DEST=${HOST_DEPLOY_ROOT}/${{ gitea.event.repository.name }}/${VERSION}"
23
+ echo "COMPOSE_DIR=${DEPLOY_ROOT}/${{ gitea.event.repository.name }}/${VERSION}/.docker/${DEPLOY_TYPE}/compose"
24
+ } >> "$GITEA_ENV"
25
+
26
+ - name: Дерегистрация записей версии в consul
27
+ run: |
28
+ SVC_NAME="${VERSION}-${PROJECT}-${PLACEHOLDER}"
29
+ FOUND=0
30
+ curl -sS -H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}" \
31
+ "${CONSUL_HTTP_ADDR}/v1/agent/services" | jq -r --arg name "$SVC_NAME" \
32
+ 'to_entries[] | select(.value.Service == $name) | .key' | \
33
+ while read -r sid; do
34
+ echo "Дерегистрирую: $sid"
35
+ curl -sS -H "X-Consul-Token: ${CONSUL_HTTP_TOKEN}" \
36
+ -X PUT "${CONSUL_HTTP_ADDR}/v1/agent/service/deregister/${sid}"
37
+ done
38
+
39
+ - name: Остановка контейнеров
40
+ run: |
41
+ if [ -d "$COMPOSE_DIR" ]; then
42
+ docker compose -f "$COMPOSE_DIR/docker-compose.yaml" -p "${PROJECT}_${VERSION}" down --remove-orphans
43
+ else
44
+ echo "compose-каталог не найден, останавливаю по метке проекта"
45
+ docker ps -q --filter "label=com.docker.compose.project=${PROJECT}_${VERSION}" \
46
+ | xargs -r docker rm -f
47
+ fi
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## v1.10.0
2
+
3
+ - Табы: атрибут data-tabs-native для нативных ссылок
4
+
5
+ ## v1.9.0
6
+
7
+ - Табы: поддержка тегов кроме `button`
8
+
1
9
  ## v1.8.0
2
10
 
3
11
  - Возвращаем список файлов при событии upload в FileInput
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formixru/b2b-ui",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,14 +19,25 @@ const setLeftWidth = async ($line, $el) => {
19
19
  }).finished;
20
20
  };
21
21
 
22
+ const setActive = ($el, active) => {
23
+ if ($el.tagName === 'BUTTON') {
24
+ $el.disabled = active;
25
+ } else if (active) {
26
+ $el.dataset.disabled = '';
27
+ } else {
28
+ delete $el.dataset.disabled;
29
+ }
30
+ };
31
+
22
32
  export const setInitLineStyles = ($b) => {
23
33
  const $nav = $b.querySelector('.tabs__nav');
24
34
  const $item = $nav.querySelectorAll('.tabs__item');
25
35
  const $line = $nav.querySelector('.tabs__line');
26
36
 
27
- const $el = [...$item].find((el) => {
28
- return el.disabled || (el.tagName !== 'BUTTON' && !el.href);
29
- });
37
+ const $el =
38
+ [...$item].find((el) => {
39
+ return el.tagName === 'BUTTON' ? el.disabled : el.dataset.disabled !== undefined;
40
+ }) || $item[0];
30
41
 
31
42
  setLeftWidth($line, $el);
32
43
  // Прокручиваем до нужного таба
@@ -54,83 +65,71 @@ export const init = async ($b) => {
54
65
  const handleItemClick = async (e) => {
55
66
  const $el = e.currentTarget;
56
67
  const index = [...$item].indexOf($el);
57
- if ($el.tagName === 'BUTTON') {
58
- if ($el.dataset.tabsFetch && $content[index].innerHTML.trim() === '') {
59
- showLoader();
60
- try {
61
- const response = await fetch($el.dataset.tabsFetch, {
62
- method: 'GET',
63
- headers: {
64
- 'X-Requested-With': 'XMLHttpRequest',
65
- 'X-Session-Id': BX.message('bitrix_sessid'),
66
- },
67
- });
68
- if (!response.ok) {
69
- throw response;
70
- }
71
- const data = await response.text();
72
- hideLoader();
73
- // Вставляем полученный HTML
74
- $content[index].innerHTML = data;
75
- // Переключаем таб
76
- for (const i of $item) i.disabled = false;
77
- $el.disabled = true;
78
- for (const [i, c] of $content.entries()) {
79
- if (i === index) {
80
- c.hidden = false;
81
- for (const swiperEl of c.querySelectorAll('.swiper')) {
82
- if (swiperEl.swiper) {
83
- swiperEl.swiper.update();
84
- }
85
- }
86
- for (const tabsEl of c.querySelectorAll('.tabs')) {
87
- setInitLineStyles(tabsEl);
88
- }
89
- // Инициализируем полученный HTML
90
- initTabsContent($content[index]);
91
- } else {
92
- c.hidden = true;
93
- }
94
- }
95
- } catch (error) {
96
- hideLoader();
97
- if (error.status === 401) {
98
- location.reload();
99
- } else {
100
- errorHandler(error);
101
- }
68
+
69
+ // Ссылка с нативным поведением — отдаём клик браузеру
70
+ if ($el.dataset.tabsNative !== undefined) return;
71
+
72
+ if ($el.tagName === 'A') {
73
+ e.preventDefault();
74
+ }
75
+
76
+ if ($el.dataset.tabsFetch && $content[index].innerHTML.trim() === '') {
77
+ showLoader();
78
+ try {
79
+ const response = await fetch($el.dataset.tabsFetch, {
80
+ method: 'GET',
81
+ headers: {
82
+ 'X-Requested-With': 'XMLHttpRequest',
83
+ 'X-Session-Id': BX.message('bitrix_sessid'),
84
+ },
85
+ });
86
+ if (!response.ok) {
87
+ throw response;
102
88
  }
103
- } else {
104
- // Переключаем таб
105
- for (const i of $item) i.disabled = false;
106
- $el.disabled = true;
107
- for (const [i, c] of $content.entries()) {
108
- if (i === index) {
109
- c.hidden = false;
110
- for (const swiperEl of c.querySelectorAll('.swiper')) {
111
- if (swiperEl.swiper) {
112
- swiperEl.swiper.update();
113
- }
114
- }
115
- for (const tabsEl of c.querySelectorAll('.tabs')) {
116
- setInitLineStyles(tabsEl);
117
- }
118
- // Инициализируем полученный HTML
119
- initTabsContent($content[index]);
120
- } else {
121
- c.hidden = true;
122
- }
89
+ const data = await response.text();
90
+ hideLoader();
91
+ // Вставляем полученный HTML
92
+ $content[index].innerHTML = data;
93
+ } catch (error) {
94
+ hideLoader();
95
+ if (error.status === 401) {
96
+ location.reload();
97
+ } else {
98
+ errorHandler(error);
123
99
  }
100
+ return;
124
101
  }
102
+ }
125
103
 
126
- if ($nav.swiper) {
127
- $nav.swiper.slideTo(index);
128
- }
104
+ // Переключаем таб
105
+ for (const i of $item) setActive(i, false);
106
+ setActive($el, true);
129
107
 
130
- if ($line) {
131
- setLeftWidth($line, $el);
108
+ for (const [i, c] of $content.entries()) {
109
+ if (i === index) {
110
+ c.hidden = false;
111
+ for (const swiperEl of c.querySelectorAll('.swiper')) {
112
+ if (swiperEl.swiper) {
113
+ swiperEl.swiper.update();
114
+ }
115
+ }
116
+ for (const tabsEl of c.querySelectorAll('.tabs')) {
117
+ setInitLineStyles(tabsEl);
118
+ }
119
+ // Инициализируем полученный HTML
120
+ initTabsContent($content[index]);
121
+ } else {
122
+ c.hidden = true;
132
123
  }
133
124
  }
125
+
126
+ if ($nav.swiper) {
127
+ $nav.swiper.slideTo(index);
128
+ }
129
+
130
+ if ($line) {
131
+ setLeftWidth($line, $el);
132
+ }
134
133
  };
135
134
 
136
135
  if ($nav.swiper) {
@@ -102,6 +102,11 @@ $b: '.tabs';
102
102
  &.swiper-slide {
103
103
  width: auto;
104
104
  }
105
+
106
+ &[data-disabled] {
107
+ color: $font-color-primary;
108
+ pointer-events: none;
109
+ }
105
110
  }
106
111
 
107
112
  &__label {