DEV Community

Kishan Srivastava
Kishan Srivastava

Posted on

iptester2

import React, { useState, useCallback } from 'react';
import { useUnleashContext, useUnleashClient } from '@unleash/proxy-client-react';
import styled, { keyframes, css } from 'styled-components';

// --- Styled Components ---

const fadeIn = keyframes`
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
`;

const spin = keyframes`
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
`;

const Container = styled.div`
  font-family: 'Inter', sans-serif;
  background-color: #f8fafc; /* Tailwind gray-50 */
  min-height: 100vh;
  padding: 2rem;
  display: flex;
  justify-content: center;
  align-items: flex-start;
`;

const MaxWidthWrapper = styled.div`
  max-width: 1200px; /* Increased max-width for more cards */
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 1.5rem; /* gap-6 */
`;

const Card = styled.div`
  background-color: white;
  border-radius: 0.75rem; /* rounded-xl */
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-xl */
  padding: 1.5rem; /* p-6 */
  border: 1px solid #e2e8f0; /* border-gray-200 */
`;

const Header = styled.div`
  text-align: center;
  margin-bottom: 1rem; /* mb-4 */
`;

const Title = styled.h1`
  font-size: 1.875rem; /* text-3xl */
  font-weight: 700; /* font-bold */
  color: #1a202c; /* gray-900 */
  margin-bottom: 0.5rem; /* mb-2 */
`;

const Subtitle = styled.p`
  font-size: 1.125rem; /* text-lg */
  color: #4a5568; /* gray-600 */
`;

const SectionTitle = styled.h2`
  font-size: 1.5rem; /* text-2xl */
  font-weight: 600; /* font-semibold */
  color: #2d3748; /* gray-800 */
  margin-bottom: 1rem; /* mb-4 */
`;

const InputGroup = styled.div`
  display: flex;
  flex-direction: column;
  gap: 1rem; /* gap-4 */
  @media (min-width: 640px) { /* sm */
    flex-direction: row;
    align-items: flex-end;
  }
`;

const InputField = styled.div`
  flex: 1;
`;

const Label = styled.label`
  display: block;
  font-size: 0.875rem; /* text-sm */
  font-weight: 500; /* font-medium */
  color: #2d3748; /* gray-700 */
  margin-bottom: 0.25rem; /* mb-1 */
`;

const Input = styled.input`
  width: 100%;
  padding: 0.75rem 1rem; /* px-4 py-3 */
  border: 1px solid #cbd5e0; /* border-gray-300 */
  border-radius: 0.5rem; /* rounded-lg */
  font-size: 1rem; /* text-base */
  color: #2d3748; /* text-gray-800 */
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); /* shadow-sm */
  &:focus {
    outline: none;
    border-color: #3b82f6; /* border-blue-500 */
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); /* ring-2 ring-blue-300 */
  }
`;

const Button = styled.button`
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem; /* gap-2 */
  padding: 0.75rem 1.5rem; /* px-6 py-3 */
  background-color: #3b82f6; /* blue-500 */
  color: white;
  font-weight: 600; /* font-semibold */
  border-radius: 0.5rem; /* rounded-lg */
  border: none;
  cursor: pointer;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */
  transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
  white-space: nowrap; /* Prevent text wrapping */

  &:hover {
    background-color: #2563eb; /* blue-600 */
    box-shadow: 0 6px 8px -1px rgba(0, 0, 0, 0.15), 0 4px 6px -2px rgba(0, 0, 0, 0.08);
  }

  &:disabled {
    background-color: #9ca3af; /* gray-400 */
    cursor: not-allowed;
    box-shadow: none;
  }
`;

const Spinner = styled.div`
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-top: 4px solid white;
  border-radius: 50%;
  width: 1.5rem; /* h-6 w-6 */
  height: 1.5rem; /* h-6 w-6 */
  animation: ${spin} 1s linear infinite;
`;

const ErrorMessage = styled.p`
  color: #dc2626; /* red-600 */
  font-size: 0.875rem; /* text-sm */
  margin-top: 0.75rem; /* mt-3 */
  text-align: center;
`;

const IpCardsGrid = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* Responsive grid */
  gap: 1.5rem; /* gap-6 */
  margin-top: 1.5rem; /* mt-6 */
`;

const IpCardContainer = styled.div`
  background-color: white;
  border-radius: 0.75rem; /* rounded-xl */
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */
  padding: 1.25rem; /* p-5 */
  border: 1px solid #e2e8f0; /* border-gray-200 */
  display: flex;
  flex-direction: column;
  gap: 0.75rem; /* gap-3 */
  animation: ${fadeIn} 0.5s ease-out;
`;

const IpCardHeader = styled.h3`
  font-size: 1.25rem; /* text-xl */
  font-weight: 600; /* font-semibold */
  color: #2d3748; /* gray-800 */
  margin-bottom: 0.5rem;
`;

const IpCardDetail = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.9375rem; /* text-base */
  color: #4a5568; /* gray-600 */
`;

const IpCardLabel = styled.span`
  font-weight: 500;
`;

const IpCardValue = styled.span`
  font-weight: 700;
  color: #1a202c;
`;

const StatusBadge = styled.span`
  display: inline-flex;
  padding: 0.25rem 0.75rem; /* px-3 py-1 */
  border-radius: 9999px; /* rounded-full */
  font-size: 0.875rem; /* text-sm */
  font-weight: 600; /* font-semibold */
  ${(props) => {
    switch (props.type) {
      case 'enabled':
        return css`
          background-color: #dcfce7; /* green-100 */
          color: #16a34a; /* green-700 */
        `;
      case 'disabled':
        return css`
          background-color: #fee2e2; /* red-100 */
          color: #dc2626; /* red-700 */
        `;
      case 'loading':
        return css`
          background-color: #eff6ff; /* blue-50 */
          color: #2563eb; /* blue-600 */
        `;
      case 'error':
        return css`
          background-color: #fee2e2; /* red-100 */
          color: #dc2626; /* red-700 */
        `;
      default:
        return css`
          background-color: #e2e8f0;
          color: #4a5568;
        `;
    }
  }}
`;

const TestButton = styled(Button)`
  width: 100%;
  margin-top: 1rem; /* mt-4 */
  background-color: #4f46e5; /* indigo-600 */
  &:hover {
    background-color: #4338ca; /* indigo-700 */
  }
`;

const EmptyState = styled.div`
  text-align: center;
  padding: 3rem; /* p-12 */
  background-color: white;
  border-radius: 0.75rem; /* rounded-xl */
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-xl */
  border: 1px solid #e2e8f0; /* border-gray-200 */
  animation: ${fadeIn} 0.5s ease-out;
`;

const EmptyStateIcon = styled.span`
  font-size: 3rem; /* text-5xl */
  margin-bottom: 1rem; /* mb-4 */
  display: block;
`;

const EmptyStateTitle = styled.h3`
  font-size: 1.5rem; /* text-2xl */
  font-weight: 600; /* font-semibold */
  color: #2d3748; /* gray-800 */
  margin-bottom: 0.5rem; /* mb-2 */
`;

const EmptyStateText = styled.p`
  font-size: 1rem; /* text-base */
  color: #4a5568; /* gray-600 */
  margin-bottom: 1rem; /* mb-4 */
`;

const EmptyStateNote = styled.p`
  font-size: 0.875rem; /* text-sm */
  color: #718096; /* gray-500 */
  font-style: italic;
`;

// --- Helper Functions for IP Conversion ---
// These are crucial for handling IP ranges

const ipToNumber = (ip) => {
  return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; // >>> 0 converts to unsigned 32-bit
};

const numberToIp = (num) => {
  return [
    (num >>> 24) & 0xFF,
    (num >>> 16) & 0xFF,
    (num >>> 8) & 0xFF,
    num & 0xFF
  ].join('.');
};

// --- IP Card Component ---
// This component represents a single IP address card

const IpCard = ({ ipData, featureFlagName, onTestFlag }) => {
  const { id, ipAddress, testResult } = ipData;

  const handleTestClick = () => {
    onTestFlag(id, ipAddress);
  };

  return (
    <IpCardContainer>
      <IpCardHeader>{ipAddress}</IpCardHeader>
      <IpCardDetail>
        <IpCardLabel>Status:</IpCardLabel>
        {testResult ? (
          testResult.isLoading ? (
            <StatusBadge type="loading">
              <Spinner style={{ width: '1rem', height: '1rem', borderTopColor: '#2563eb' }} /> Testing...
            </StatusBadge>
          ) : testResult.error ? (
            <StatusBadge type="error">❌ Error</StatusBadge>
          ) : testResult.enabled ? (
            <StatusBadge type="enabled">βœ… Enabled</StatusBadge>
          ) : (
            <StatusBadge type="disabled">❌ Disabled</StatusBadge>
          )
        ) : (
          <StatusBadge>Not Tested</StatusBadge>
        )}
      </IpCardDetail>
      {testResult && testResult.timestamp && (
        <IpCardDetail>
          <IpCardLabel>Last Test:</IpCardLabel>
          <IpCardValue>{testResult.timestamp}</IpCardValue>
        </IpCardDetail>
      )}
      <TestButton onClick={handleTestClick} disabled={testResult?.isLoading}>
        {testResult?.isLoading ? 'Testing...' : 'Test Flag'}
      </TestButton>
    </IpCardContainer>
  );
};

// --- Main IP Range Simulator Component ---

const IPRangeSimulator = () => {
  const [featureFlagName, setFeatureFlagName] = useState('geo-targeted-feature');
  const [startIp, setStartIp] = useState('192.168.1.1');
  const [endIp, setEndIp] = useState('192.168.1.10');
  const [discreteIp, setDiscreteIp] = useState('');
  const [ipCards, setIpCards] = useState([]); // Array to hold all IP card data
  const [errorMessage, setErrorMessage] = useState('');

  const unleashClient = useUnleashClient();
  const updateContext = useUnleashContext();

  // Function to add IPs from a range
  const addRangeCards = useCallback(() => {
    setErrorMessage('');
    try {
      const startNum = ipToNumber(startIp);
      const endNum = ipToNumber(endIp);

      if (isNaN(startNum) || isNaN(endNum) || startNum > endNum) {
        setErrorMessage('Invalid IP range. Please enter valid start and end IPs.');
        return;
      }

      const newCards = [];
      for (let i = startNum; i <= endNum; i++) {
        newCards.push({
          id: crypto.randomUUID(), // Unique ID for each card
          ipAddress: numberToIp(i),
          testResult: null, // Initial state for test result
        });
        if (newCards.length > 100) { // Limit to prevent too many cards
            setErrorMessage('Too many IPs in range. Max 100 cards allowed per range generation.');
            break;
        }
      }
      setIpCards(prevCards => [...prevCards, ...newCards]);
      setStartIp(''); // Clear inputs after adding
      setEndIp('');
    } catch (e) {
      setErrorMessage('Error parsing IP range. Please check format.');
      console.error(e);
    }
  }, [startIp, endIp]);

  // Function to add a discrete IP
  const addDiscreteCard = useCallback(() => {
    setErrorMessage('');
    if (!discreteIp || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(discreteIp)) {
      setErrorMessage('Please enter a valid discrete IP address.');
      return;
    }
    setIpCards(prevCards => [
      ...prevCards,
      {
        id: crypto.randomUUID(),
        ipAddress: discreteIp,
        testResult: null,
      },
    ]);
    setDiscreteIp(''); // Clear input after adding
  }, [discreteIp]);

  // Function to handle testing a single IP flag (called by IpCard)
  const handleTestFlag = useCallback(async (cardId, ipAddress) => {
    setIpCards(prevCards =>
      prevCards.map(card =>
        card.id === cardId ? { ...card, testResult: { ...card.testResult, isLoading: true, error: false } } : card
      )
    );
    setErrorMessage(''); // Clear general error message

    try {
      await updateContext({
        remoteAddress: ipAddress,
        userId: 'simulator-user-' + ipAddress.replace(/\./g, '-'), // Ensure unique userId per IP test
      });
      await new Promise((resolve) => setTimeout(resolve, 100)); // Small delay

      const isEnabled = unleashClient.isEnabled(featureFlagName);

      setIpCards(prevCards =>
        prevCards.map(card =>
          card.id === cardId
            ? {
                ...card,
                testResult: {
                  ip: ipAddress,
                  enabled: isEnabled,
                  timestamp: new Date().toLocaleTimeString(),
                  isLoading: false,
                  error: false,
                },
              }
            : card
        )
      );
    } catch (error) {
      console.error(`Error testing flag for IP ${ipAddress}:`, error);
      setIpCards(prevCards =>
        prevCards.map(card =>
          card.id === cardId
            ? {
                ...card,
                testResult: {
                  ip: ipAddress,
                  enabled: false,
                  timestamp: new Date().toLocaleTimeString(),
                  isLoading: false,
                  error: true,
                },
              }
            : card
        )
      );
      setErrorMessage(`Failed to test flag for ${ipAddress}. Check console.`);
    }
  }, [featureFlagName, updateContext, unleashClient]);

  return (
    <Container>
      <MaxWidthWrapper>
        {/* Header */}
        <Card>
          <Header>
            <Title>πŸ—ΊοΈ Unleash IP Strategy Simulator</Title>
            <Subtitle>
              Generate and test feature flag status for multiple IP addresses.
            </Subtitle>
          </Header>
        </Card>

        {/* Configuration Panel */}
        <Card>
          <SectionTitle>Configuration & IP Generation</SectionTitle>
          <InputField className="mb-4">
            <Label htmlFor="flagName">Feature Flag Name</Label>
            <Input
              id="flagName"
              type="text"
              value={featureFlagName}
              onChange={(e) => setFeatureFlagName(e.target.value)}
              placeholder="e.g., geo-targeted-feature"
            />
          </InputField>

          <div className="flex flex-col gap-4 mb-4 md:flex-row">
            <InputField>
              <Label htmlFor="startIp">Start IP (Range)</Label>
              <Input
                id="startIp"
                type="text"
                value={startIp}
                onChange={(e) => setStartIp(e.target.value)}
                placeholder="e.g., 192.168.1.1"
              />
            </InputField>
            <InputField>
              <Label htmlFor="endIp">End IP (Range)</Label>
              <Input
                id="endIp"
                type="text"
                value={endIp}
                onChange={(e) => setEndIp(e.target.value)}
                placeholder="e.g., 192.168.1.10"
              />
            </InputField>
            <Button onClick={addRangeCards}>
              Generate Range Cards
            </Button>
          </div>

          <div className="flex flex-col gap-4 md:flex-row">
            <InputField>
              <Label htmlFor="discreteIp">Discrete IP Address</Label>
              <Input
                id="discreteIp"
                type="text"
                value={discreteIp}
                onChange={(e) => setDiscreteIp(e.target.value)}
                placeholder="e.g., 203.0.113.45"
              />
            </InputField>
            <Button onClick={addDiscreteCard}>
              Add Discrete IP Card
            </Button>
          </div>

          {errorMessage && (
            <ErrorMessage>{errorMessage}</ErrorMessage>
          )}
        </Card>

        {/* IP Cards Display */}
        {ipCards.length > 0 ? (
          <IpCardsGrid>
            {ipCards.map((ipData) => (
              <IpCard
                key={ipData.id}
                ipData={ipData}
                featureFlagName={featureFlagName}
                onTestFlag={handleTestFlag}
              />
            ))}
          </IpCardsGrid>
        ) : (
          <EmptyState>
            <EmptyStateIcon>βž•</EmptyStateIcon>
            <EmptyStateTitle>Start by Adding IPs</EmptyStateTitle>
            <EmptyStateText>
              Enter an IP range or a discrete IP address to generate cards and begin testing.
            </EmptyStateText>
            <EmptyStateNote>
              Ensure your feature flag "{featureFlagName}" has an "IP Address" strategy configured in Unleash.
            </EmptyStateNote>
          </EmptyState>
        )}
      </MaxWidthWrapper>
    </Container>
  );
};

export default IPRangeSimulator;


Enter fullscreen mode Exit fullscreen mode

Top comments (0)