ai

Radix UI Primitives กับ Progressive Delivery —

Radix UI Primitives กับ Progressive Delivery —

Radix UI Primitives

Radix UI Primitives กับ Progressive Delivery —

Radix UI Primitives เป็น UI Component Library สำหรับ React ที่ออกแบบมาให้เป็น Unstyled Components ให้เฉพาะ Behavior และ Accessibility (WAI-ARIA) ที่ถูกต้อง Developer สามารถ Style เองได้อิสระ 100% ใช้ร่วมกับ Tailwind CSS, CSS Modules หรือ Styled Components

เนื้อหาเกี่ยวข้อง — บทความที่เกี่ยวข้อง: Strength แปลว่าอะไร

เมื่อรวมกับ Progressive Delivery Strategy ใช้ Feature Flags ควบคุมการปล่อย Components ใหม่ให้ผู้ใช้ทีละกลุ่ม Canary Deployment ทดสอบกับผู้ใช้ส่วันนี้อยก่อน A/B Testing เปรียบเทียบ Design ต่างๆ

เนื้อหาเกี่ยวข้อง — แนะนำให้อ่าน แท็ก html คืออะไรใช้งานอย่างไร

Radix UI Components พร้อม Tailwind CSS

// === Radix UI + Tailwind CSS Components ===
// npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
// npm install @radix-ui/react-tabs @radix-ui/react-tooltip
// npm install @radix-ui/react-switch @radix-ui/react-select
// npm install tailwindcss class-variance-authority clsx

// components/Dialog.tsx — Accessible Modal Dialog
import * as Dialog from "@radix-ui/react-dialog";
import { X } from "lucide-react";

interface ModalProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description?: string;
  children: React.ReactNode;
}

export function Modal({ open, onOpenChange, title, description, children }: ModalProps) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm
          data-[state=open]:animate-in data-[state=closed]:animate-out
          data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
        <Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2
          -translate-y-1/2 bg-white rounded-xl shadow-xl p-6 w-full max-w-md
          data-[state=open]:animate-in data-[state=closed]:animate-out
          data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
          data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95">
          <Dialog.Title className="text-lg font-semibold text-gray-900">
            {title}
          </Dialog.Title>
          {description && (
            <Dialog.Description className="mt-2 text-sm text-gray-500">
              {description}
            </Dialog.Description>
          )}
          <div className="mt-4">{children}</div>
          <Dialog.Close className="absolute right-4 top-4 rounded-sm
            opacity-70 hover:opacity-100 transition-opacity">
            <X className="h-4 w-4" />
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

// components/Tabs.tsx — Accessible Tabs
import * as Tabs from "@radix-ui/react-tabs";

interface TabItem {
  value: string;
  label: string;
  content: React.ReactNode;
}

export function TabGroup({ tabs, defaultValue }: { tabs: TabItem[]; defaultValue?: string }) {
  return (
    <Tabs.Root defaultValue={defaultValue || tabs[0]?.value}>
      <Tabs.List className="flex border-b border-gray-200">
        {tabs.map((tab) => (
          <Tabs.Trigger key={tab.value} value={tab.value}
            className="px-4 py-2 text-sm font-medium text-gray-500
              hover:text-gray-700 border-b-2 border-transparent
              data-[state=active]:border-blue-500
              data-[state=active]:text-blue-600 transition-colors">
            {tab.label}
          </Tabs.Trigger>
        ))}
      </Tabs.List>
      {tabs.map((tab) => (
        <Tabs.Content key={tab.value} value={tab.value} className="py-4">
          {tab.content}
        </Tabs.Content>
      ))}
    </Tabs.Root>
  );
}

// components/DropdownMenu.tsx — Accessible Dropdown
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

interface MenuItem {
  label: string;
  onClick: () => void;
  icon?: React.ReactNode;
  destructive?: boolean;
}

export function Dropdown({ trigger, items }: { trigger: React.ReactNode; items: MenuItem[] }) {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="min-w-[180px] bg-white rounded-lg
          shadow-lg border border-gray-200 p-1 animate-in fade-in-0 zoom-in-95">
          {items.map((item, i) => (
            <DropdownMenu.Item key={i} onClick={item.onClick}
              className={`flex items-center gap-2 px-3 py-2 text-sm rounded-md
                cursor-pointer outline-none
                `}>
              {item.icon} {item.label}
            </DropdownMenu.Item>
          ))}
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

Feature Flags สำหรับ Progressive Delivery

Radix UI Primitives กับ Progressive Delivery —
# feature_flags.py — Feature Flag System สำหรับ Progressive Delivery
import hashlib
import json
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from enum import Enum

class RolloutStrategy(Enum):
    ALL = "all"
    PERCENTAGE = "percentage"
    USER_LIST = "user_list"
    GRADUAL = "gradual"

@dataclass
class FeatureFlag:
    name: str
    enabled: bool
    strategy: RolloutStrategy
    percentage: float = 0
    user_list: List[str] = field(default_factory=list)
    gradual_schedule: Dict[str, float] = field(default_factory=dict)
    description: str = ""
    created_at: float = field(default_factory=time.time)

class FeatureFlagService:
    """Feature Flag Service สำหรับ Progressive Delivery"""

    def __init__(self):
        self.flags: Dict[str, FeatureFlag] = {}
        self.evaluations: Dict[str, int] = {}

    def create_flag(self, name, strategy=RolloutStrategy.ALL,
                    percentage=0, user_list=None, description=""):
        flag = FeatureFlag(
            name=name, enabled=True, strategy=strategy,
            percentage=percentage,
            user_list=user_list or [],
            description=description,
        )
        self.flags[name] = flag
        return flag

    def is_enabled(self, flag_name, user_id=None, attributes=None):
        """ตรวจสอบว่า Feature เปิดสำหรับ User นี้หรือไม่"""
        flag = self.flags.get(flag_name)
        if not flag or not flag.enabled:
            return False

        # Track evaluations
        self.evaluations[flag_name] = self.evaluations.get(flag_name, 0) + 1

        if flag.strategy == RolloutStrategy.ALL:
            return True

        elif flag.strategy == RolloutStrategy.PERCENTAGE:
            if not user_id:
                return False
            hash_val = int(hashlib.md5(
                f"{flag_name}:{user_id}".encode()
            ).hexdigest(), 16)
            return (hash_val % 100) < flag.percentage

        elif flag.strategy == RolloutStrategy.USER_LIST:
            return user_id in flag.user_list

        elif flag.strategy == RolloutStrategy.GRADUAL:
            current_pct = self._get_current_percentage(flag)
            if not user_id:
                return False
            hash_val = int(hashlib.md5(
                f"{flag_name}:{user_id}".encode()
            ).hexdigest(), 16)
            return (hash_val % 100) < current_pct

        return False

    def _get_current_percentage(self, flag):
        """คำนวณ Percentage ปัจจุบันตาม Schedule"""
        now = time.time()
        current_pct = 0
        for ts_str, pct in sorted(flag.gradual_schedule.items()):
            if float(ts_str) <= now:
                current_pct = pct
        return current_pct

    def set_percentage(self, flag_name, percentage):
        """ปรับ Percentage"""
        if flag_name in self.flags:
            self.flags[flag_name].percentage = percentage

    def kill_switch(self, flag_name):
        """ปิด Feature ทันที"""
        if flag_name in self.flags:
            self.flags[flag_name].enabled = False

    def dashboard(self):
        """แสดง Feature Flags Dashboard"""
        print(f"\n{'='*60}")
        print(f"Feature Flags Dashboard")
        print(f"{'='*60}")

        for name, flag in self.flags.items():
            status = "ON" if flag.enabled else "OFF"
            evals = self.evaluations.get(name, 0)
            print(f"\n  [{status:>3}] {name}")
            print(f"    Strategy: {flag.strategy.value}")
            if flag.strategy == RolloutStrategy.PERCENTAGE:
                print(f"    Percentage: {flag.percentage}%")
            elif flag.strategy == RolloutStrategy.USER_LIST:
                print(f"    Users: {len(flag.user_list)}")
            print(f"    Evaluations: {evals}")
            if flag.description:
                print(f"    Description: {flag.description}")

# === ตัวอย่าง ===
ff = FeatureFlagService()

# Canary: เปิดให้ 5% ก่อน
ff.create_flag("new_search_ui", RolloutStrategy.PERCENTAGE,
               percentage=5, description="New Search UI with Radix Components")

# Beta Users
ff.create_flag("dark_mode", RolloutStrategy.USER_LIST,
               user_list=["user_001", "user_002", "user_003"],
               description="Dark Mode Feature")

# ทดสอบ
for i in range(100):
    user = f"user_{i:03d}"
    ff.is_enabled("new_search_ui", user)
    ff.is_enabled("dark_mode", user)

ff.dashboard()

# ค่อยๆเพิ่ม: 5% -> 25% -> 50% -> 100%
ff.set_percentage("new_search_ui", 25)
print("\nIncreased new_search_ui to 25%")

CI/CD Pipeline สำหรับ Progressive Delivery

# === GitHub Actions — Progressive Delivery Pipeline ===
# .github/workflows/progressive-delivery.yml

name: Progressive Delivery
on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: myapp-frontend

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm run test -- --coverage

      # Accessibility Tests
      - run: npm run test:a11y
      - run: npx playwright test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t $REGISTRY/$IMAGE_NAME:} .
      - run: docker push $REGISTRY/$IMAGE_NAME:}

  canary:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy Canary (5%)
        run: |
          kubectl set image deployment/frontend-canary \
            frontend=$REGISTRY/$IMAGE_NAME:} \
            -n production
          # Istio Traffic Split: 5% canary
          kubectl apply -f - << 'EOF'
          apiVersion: networking.istio.io/v1beta1
          kind: VirtualService
          metadata:
            name: frontend
            namespace: production
          spec:
            hosts: ["www.example.com"]
            http:
              - route:
                  - destination:
                      host: frontend-stable
                    weight: 95
                  - destination:
                      host: frontend-canary
                    weight: 5
          EOF

      - name: Monitor Canary (5 min)
        run: |
          sleep 300
          ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
            --data-urlencode 'query=rate(http_requests_total{status=~"5.."}[5m])' \
            | jq '.data.result[0].value[1]')
          if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
            echo "Canary failed! Error rate: $ERROR_RATE"
            exit 1
          fi

      - name: Promote to 50%
        run: |
          # Update traffic split to 50/50
          echo "Promoting canary to 50%"

      - name: Promote to 100%
        run: |
          kubectl set image deployment/frontend-stable \
            frontend=$REGISTRY/$IMAGE_NAME:} \
            -n production
          echo "Full rollout complete"

Best Practices

  • Unstyled Components: ใช้ Radix UI Primitives เป็นฐาน Style เองด้วย Tailwind CSS
  • Accessibility First: Radix UI มี WAI-ARIA ในตัว ทดสอบด้วย axe-core
  • Feature Flags: ใช้ Feature Flags ควบคุม Component ใหม่ เปิดปิดโดยไม่ต้อง Deploy
  • Canary Release: ปล่อย Feature ให้ผู้ใช้ 5% ก่อน Monitor Error Rate แล้วค่อยขยาย
  • Kill Switch: ทุก Feature ต้องมี Kill Switch ปิดได้ทันทีถ้ามีปัญหา
  • A/B Testing: ใช้ Feature Flags ทำ A/B Test เปรียบเทียบ UI ต่างๆ

Radix UI Primitives คืออะไร

Open-source UI Component Library สำหรับ React เน้น Accessibility Unstyled Components Composability ให้ Behavior WAI-ARIA ที่ถูกต้อง Developer Style เองได้อิสระ ใช้กับ Tailwind CSS ได้

แนะนำเพิ่มเติม — XM Signal

เนื้อหาเกี่ยวข้อง — LlamaIndex RAG Low Code No Code

XM Legend · เทรดเดอร์ & ผู้สอน Forex 13 ปี

ผู้ก่อตั้ง SiamCafe ตั้งแต่ปี 1997 · เทรดเดอร์สาย Forex มากกว่า 13 ปี ได้รับการยกย่องเป็น XM Legend · แบ่งปันความรู้ Forex, ไอที, AI และการเทรด จากประสบการณ์จริงในตลาดจริง