Add frontend, member_state_log, etc.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
-- Step 1: Create the member_states lookup table
|
||||
CREATE TABLE member_states (
|
||||
state_code CHAR(1) PRIMARY KEY,
|
||||
state_name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert the three states
|
||||
INSERT INTO member_states (state_code, state_name, description) VALUES
|
||||
('A', 'Active', 'Member is currently active'),
|
||||
('I', 'Inactive', 'Member is temporarily inactive'),
|
||||
('D', 'Deactivated', 'Member has been permanently deactivated');
|
||||
|
||||
-- Step 2: Create the member_state_log table
|
||||
CREATE TABLE member_state_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL,
|
||||
state_code CHAR(1) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
changed_by INTEGER, -- Optional: user ID who made the change
|
||||
reason TEXT, -- Optional: reason for state change
|
||||
FOREIGN KEY (member_id) REFERENCES members(member_id),
|
||||
FOREIGN KEY (state_code) REFERENCES member_states(state_code)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_member_state_log_member_id ON member_state_log(member_id);
|
||||
CREATE INDEX idx_member_state_log_changed_at ON member_state_log(changed_at);
|
||||
CREATE INDEX idx_member_state_log_state_code ON member_state_log(state_code);
|
||||
|
||||
-- Step 3: Migration script to populate the log table from existing data
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_at)
|
||||
SELECT
|
||||
member_id,
|
||||
CASE
|
||||
WHEN member_active = true THEN 'A'
|
||||
ELSE 'I'
|
||||
END,
|
||||
COALESCE(updated_at, created_at, CURRENT_TIMESTAMP)
|
||||
FROM members;
|
||||
|
||||
-- Step 4: Create a view to get current member states (most recent log entry per member)
|
||||
CREATE OR REPLACE VIEW member_current_state AS
|
||||
SELECT DISTINCT ON (msl.member_id)
|
||||
msl.member_id,
|
||||
msl.state_code,
|
||||
ms.state_name,
|
||||
ms.description,
|
||||
msl.changed_at,
|
||||
msl.changed_by,
|
||||
msl.reason
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
ORDER BY msl.member_id, msl.changed_at DESC, msl.log_id DESC;
|
||||
|
||||
-- Step 5: Function to change member state (ensures proper logging)
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 6: Common queries you'll need
|
||||
|
||||
-- Get all active members
|
||||
SELECT m.*, mcs.state_name, mcs.changed_at as last_state_change
|
||||
FROM members m
|
||||
JOIN member_current_state mcs ON m.member_id = mcs.member_id
|
||||
WHERE mcs.state_code = 'A';
|
||||
|
||||
-- Get member state history
|
||||
SELECT
|
||||
msl.changed_at,
|
||||
ms.state_name,
|
||||
msl.reason,
|
||||
msl.changed_by
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
WHERE msl.member_id = 123 -- Replace with actual member_id
|
||||
ORDER BY msl.changed_at DESC;
|
||||
|
||||
-- Count members by state
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM member_current_state mcs
|
||||
JOIN member_states ms ON mcs.state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 7: Example usage of the change_member_state function
|
||||
-- Change member 123 to inactive
|
||||
SELECT change_member_state(123, 'I', 1, 'Member requested temporary suspension');
|
||||
|
||||
-- Reactivate member 123
|
||||
SELECT change_member_state(123, 'A', 1, 'Member reactivation approved');
|
||||
|
||||
-- Step 8: After testing, remove the old column (CAREFUL!)
|
||||
-- ALTER TABLE members DROP COLUMN member_active;
|
||||
@@ -0,0 +1,252 @@
|
||||
-- Step 1: Create the member_states lookup table
|
||||
CREATE TABLE member_states (
|
||||
state_code CHAR(1) PRIMARY KEY,
|
||||
state_name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert the three states
|
||||
INSERT INTO member_states (state_code, state_name, description) VALUES
|
||||
('A', 'Active', 'Member is currently active'),
|
||||
('I', 'Inactive', 'Member is temporarily inactive'),
|
||||
('D', 'Deactivated', 'Member has been permanently deactivated');
|
||||
|
||||
-- Step 2: Create the member_state_log table
|
||||
drop table member_state_log;
|
||||
CREATE TABLE member_state_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL,
|
||||
state_code CHAR(2) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
changed_by INTEGER, -- Optional: user ID who made the change
|
||||
reason TEXT, -- Optional: reason for state change
|
||||
FOREIGN KEY (member_id) REFERENCES members(id),
|
||||
FOREIGN KEY (state_code) REFERENCES member_states(state_code)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_member_state_log_member_id ON member_state_log(member_id);
|
||||
CREATE INDEX idx_member_state_log_changed_at ON member_state_log(changed_at);
|
||||
CREATE INDEX idx_member_state_log_state_code ON member_state_log(state_code);
|
||||
|
||||
-- Step 3: Migration script to populate the log table from existing data
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_at)
|
||||
SELECT
|
||||
id,
|
||||
case
|
||||
when member_died is not null then 'D'
|
||||
when member_active = true THEN 'A'
|
||||
ELSE 'I'
|
||||
END,
|
||||
COALESCE(member_died, updated_date, created_date, CURRENT_TIMESTAMP)
|
||||
FROM members;
|
||||
|
||||
-- Step 4: Create a view to get current member states (most recent log entry per member)
|
||||
CREATE OR REPLACE VIEW member_current_state AS
|
||||
SELECT DISTINCT ON (msl.member_id)
|
||||
msl.member_id,
|
||||
msl.state_code,
|
||||
ms.state_name,
|
||||
ms.description,
|
||||
msl.changed_at,
|
||||
msl.changed_by,
|
||||
msl.reason
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
ORDER BY msl.member_id, msl.changed_at DESC, msl.log_id DESC;
|
||||
|
||||
-- Step 5: Function to change member state (ensures proper logging)
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 6: Common queries you'll need
|
||||
|
||||
-- Get all active members
|
||||
SELECT m.*, mcs.state_name, mcs.changed_at as last_state_change
|
||||
FROM members m
|
||||
JOIN member_current_state mcs ON m.member_id = mcs.member_id
|
||||
WHERE mcs.state_code = 'A';
|
||||
|
||||
-- Get member state history
|
||||
SELECT
|
||||
msl.changed_at,
|
||||
ms.state_name,
|
||||
msl.reason,
|
||||
msl.changed_by
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
WHERE msl.member_id = 123 -- Replace with actual member_id
|
||||
ORDER BY msl.changed_at DESC;
|
||||
|
||||
-- Count members by state
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM member_current_state mcs
|
||||
JOIN member_states ms ON mcs.state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 7: Example usage of the change_member_state function
|
||||
-- Change member 123 to inactive
|
||||
SELECT change_member_state(123, 'I', 1, 'Member requested temporary suspension');
|
||||
|
||||
-- Reactivate member 123
|
||||
SELECT change_member_state(123, 'A', 1, 'Member reactivation approved');
|
||||
|
||||
-- Step 8: Options for "calculated column" - current member state
|
||||
|
||||
-- OPTION 1: Add a cached state column with triggers (RECOMMENDED)
|
||||
ALTER TABLE members ADD COLUMN current_state_code CHAR(1);
|
||||
ALTER TABLE members ADD COLUMN current_state_changed_at TIMESTAMP;
|
||||
ALTER TABLE members ADD CONSTRAINT fk_members_current_state
|
||||
FOREIGN KEY (current_state_code) REFERENCES member_states(state_code);
|
||||
|
||||
-- Function to update member's current state cache
|
||||
CREATE OR REPLACE FUNCTION update_member_current_state_cache(p_member_id INTEGER)
|
||||
RETURNS VOID AS $
|
||||
DECLARE
|
||||
current_state RECORD;
|
||||
BEGIN
|
||||
-- Get the most recent state for this member
|
||||
SELECT state_code, changed_at
|
||||
INTO current_state
|
||||
FROM member_state_log
|
||||
WHERE member_id = p_member_id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Update the cached values in members table
|
||||
IF FOUND THEN
|
||||
UPDATE members
|
||||
SET current_state_code = current_state.state_code,
|
||||
current_state_changed_at = current_state.changed_at
|
||||
WHERE member_id = p_member_id;
|
||||
END IF;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to automatically update cache when state changes
|
||||
CREATE OR REPLACE FUNCTION trigger_update_member_state_cache()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
PERFORM update_member_current_state_cache(NEW.member_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tr_member_state_log_update_cache
|
||||
AFTER INSERT ON member_state_log
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_update_member_state_cache();
|
||||
|
||||
-- Initialize the cache for existing members
|
||||
UPDATE members
|
||||
SET (current_state_code, current_state_changed_at) = (
|
||||
SELECT mcs.state_code, mcs.changed_at
|
||||
FROM member_current_state mcs
|
||||
WHERE mcs.member_id = members.member_id
|
||||
);
|
||||
|
||||
-- Update the change_member_state function to work with cache
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry (trigger will update cache automatically)
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- OPTION 2: Generated column using a function (PostgreSQL 12+)
|
||||
-- Note: This approach has performance implications for large tables
|
||||
ALTER TABLE members ADD COLUMN member_state CHAR(2)
|
||||
GENERATED ALWAYS AS (
|
||||
(SELECT state_code
|
||||
FROM member_state_log msl
|
||||
WHERE msl.member_id = members.id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1)
|
||||
) STORED;
|
||||
|
||||
-- OPTION 3: Materialized view (refresh manually or on schedule)
|
||||
/*
|
||||
CREATE MATERIALIZED VIEW members_with_current_state AS
|
||||
SELECT
|
||||
m.*,
|
||||
mcs.state_code as current_state_code,
|
||||
mcs.state_name as current_state_name,
|
||||
mcs.changed_at as current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_current_state mcs ON m.member_id = mcs.member_id;
|
||||
|
||||
CREATE UNIQUE INDEX idx_members_with_current_state_id
|
||||
ON members_with_current_state(member_id);
|
||||
|
||||
-- Refresh the materialized view (call this after state changes)
|
||||
-- REFRESH MATERIALIZED VIEW members_with_current_state;
|
||||
*/
|
||||
|
||||
-- Updated common queries using the cached column (OPTION 1)
|
||||
|
||||
-- Get all active members (much faster now!)
|
||||
SELECT m.*, ms.state_name
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.current_state_code = 'A';
|
||||
|
||||
-- Get member with current state info
|
||||
SELECT
|
||||
m.*,
|
||||
ms.state_name as current_state_name,
|
||||
m.current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.member_id = 123;
|
||||
|
||||
-- Count members by current state (very fast)
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 9: After testing, remove the old column (CAREFUL!)
|
||||
-- ALTER TABLE members DROP COLUMN member_active;
|
||||
@@ -0,0 +1,255 @@
|
||||
-- Step 1: Create the member_states lookup table
|
||||
CREATE TABLE member_states (
|
||||
state_code CHAR(1) PRIMARY KEY,
|
||||
state_name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert the three states
|
||||
INSERT INTO member_states (state_code, state_name, description) VALUES
|
||||
('A', 'Active', 'Member is currently active'),
|
||||
('I', 'Inactive', 'Member is temporarily inactive'),
|
||||
('D', 'Deactivated', 'Member has been permanently deactivated');
|
||||
|
||||
-- Step 2: Create the member_state_log table
|
||||
CREATE TABLE member_state_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL,
|
||||
state_code CHAR(1) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
changed_by INTEGER, -- Optional: user ID who made the change
|
||||
reason TEXT, -- Optional: reason for state change
|
||||
FOREIGN KEY (member_id) REFERENCES members(member_id),
|
||||
FOREIGN KEY (state_code) REFERENCES member_states(state_code)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_member_state_log_member_id ON member_state_log(member_id);
|
||||
CREATE INDEX idx_member_state_log_changed_at ON member_state_log(changed_at);
|
||||
CREATE INDEX idx_member_state_log_state_code ON member_state_log(state_code);
|
||||
|
||||
-- Step 3: Migration script to populate the log table from existing data
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_at)
|
||||
SELECT
|
||||
member_id,
|
||||
CASE
|
||||
WHEN member_died IS NOT NULL THEN 'D'
|
||||
WHEN member_active = true THEN 'A'
|
||||
ELSE 'I'
|
||||
END,
|
||||
COALESCE(updated_at, created_at, CURRENT_TIMESTAMP)
|
||||
FROM members;
|
||||
|
||||
-- Step 4: Create a view to get current member states (most recent log entry per member)
|
||||
CREATE OR REPLACE VIEW member_current_state AS
|
||||
SELECT DISTINCT ON (msl.member_id)
|
||||
msl.member_id,
|
||||
msl.state_code,
|
||||
ms.state_name,
|
||||
ms.description,
|
||||
msl.changed_at,
|
||||
msl.changed_by,
|
||||
msl.reason
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
ORDER BY msl.member_id, msl.changed_at DESC, msl.log_id DESC;
|
||||
|
||||
-- Step 5: Function to change member state (ensures proper logging)
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 6: Common queries you'll need
|
||||
|
||||
-- Get all active members
|
||||
SELECT m.*, mcs.state_name, mcs.changed_at as last_state_change
|
||||
FROM members m
|
||||
JOIN member_current_state mcs ON m.member_id = mcs.member_id
|
||||
WHERE mcs.state_code = 'A';
|
||||
|
||||
-- Get member state history
|
||||
SELECT
|
||||
msl.changed_at,
|
||||
ms.state_name,
|
||||
msl.reason,
|
||||
msl.changed_by
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
WHERE msl.member_id = 123 -- Replace with actual member_id
|
||||
ORDER BY msl.changed_at DESC;
|
||||
|
||||
-- Count members by state
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM member_current_state mcs
|
||||
JOIN member_states ms ON mcs.state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 7: Example usage of the change_member_state function
|
||||
-- Change member 123 to inactive
|
||||
SELECT change_member_state(123, 'I', 1, 'Member requested temporary suspension');
|
||||
|
||||
-- Reactivate member 123
|
||||
SELECT change_member_state(123, 'A', 1, 'Member reactivation approved');
|
||||
|
||||
-- Step 8: Options for "calculated column" - current member state
|
||||
|
||||
-- OPTION 1: Add a cached state column with triggers (RECOMMENDED)
|
||||
ALTER TABLE members ADD COLUMN current_state_code CHAR(1);
|
||||
ALTER TABLE members ADD COLUMN current_state_changed_at TIMESTAMP;
|
||||
ALTER TABLE members ADD CONSTRAINT fk_members_current_state
|
||||
FOREIGN KEY (current_state_code) REFERENCES member_states(state_code);
|
||||
|
||||
-- Function to update member's current state cache
|
||||
CREATE OR REPLACE FUNCTION update_member_current_state_cache(p_member_id INTEGER)
|
||||
RETURNS VOID AS $
|
||||
DECLARE
|
||||
current_state RECORD;
|
||||
BEGIN
|
||||
-- Get the most recent state for this member
|
||||
SELECT state_code, changed_at
|
||||
INTO current_state
|
||||
FROM member_state_log
|
||||
WHERE member_id = p_member_id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Update the cached values in members table
|
||||
IF FOUND THEN
|
||||
UPDATE members
|
||||
SET current_state_code = current_state.state_code,
|
||||
current_state_changed_at = current_state.changed_at
|
||||
WHERE member_id = p_member_id;
|
||||
END IF;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to automatically update cache when state changes
|
||||
CREATE OR REPLACE FUNCTION trigger_update_member_state_cache()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
PERFORM update_member_current_state_cache(NEW.member_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tr_member_state_log_update_cache
|
||||
AFTER INSERT ON member_state_log
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_update_member_state_cache();
|
||||
|
||||
-- Initialize the cache for existing members
|
||||
UPDATE members
|
||||
SET (current_state_code, current_state_changed_at) = (
|
||||
SELECT mcs.state_code, mcs.changed_at
|
||||
FROM member_current_state mcs
|
||||
WHERE mcs.member_id = members.member_id
|
||||
);
|
||||
|
||||
-- Update the change_member_state function to work with cache
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry (trigger will update cache automatically)
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- OPTION 2: Generated column using a function (NOT POSSIBLE)
|
||||
-- PostgreSQL doesn't allow subqueries in generated column expressions
|
||||
-- This approach won't work:
|
||||
/*
|
||||
ALTER TABLE members ADD COLUMN current_state_code_generated CHAR(1)
|
||||
GENERATED ALWAYS AS (
|
||||
(SELECT state_code
|
||||
FROM member_state_log msl
|
||||
WHERE msl.member_id = members.member_id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1)
|
||||
) STORED;
|
||||
-- ERROR: cannot use subquery in column generation expression
|
||||
*/
|
||||
|
||||
-- OPTION 3: Materialized view (refresh manually or on schedule)
|
||||
/*
|
||||
CREATE MATERIALIZED VIEW members_with_current_state AS
|
||||
SELECT
|
||||
m.*,
|
||||
mcs.state_code as current_state_code,
|
||||
mcs.state_name as current_state_name,
|
||||
mcs.changed_at as current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_current_state mcs ON m.member_id = mcs.member_id;
|
||||
|
||||
CREATE UNIQUE INDEX idx_members_with_current_state_id
|
||||
ON members_with_current_state(member_id);
|
||||
|
||||
-- Refresh the materialized view (call this after state changes)
|
||||
-- REFRESH MATERIALIZED VIEW members_with_current_state;
|
||||
*/
|
||||
|
||||
-- Updated common queries using the cached column (OPTION 1)
|
||||
|
||||
-- Get all active members (much faster now!)
|
||||
SELECT m.*, ms.state_name
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.current_state_code = 'A';
|
||||
|
||||
-- Get member with current state info
|
||||
SELECT
|
||||
m.*,
|
||||
ms.state_name as current_state_name,
|
||||
m.current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.member_id = 123;
|
||||
|
||||
-- Count members by current state (very fast)
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 9: After testing, remove the old column (CAREFUL!)
|
||||
-- ALTER TABLE members DROP COLUMN member_active;
|
||||
@@ -0,0 +1,197 @@
|
||||
from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# OPTION 1: Correct relationship setup
|
||||
class MemberStates(Base):
|
||||
__tablename__ = 'member_states'
|
||||
|
||||
state_code = Column(String(1), primary_key=True)
|
||||
state_name = Column(String(20), nullable=False)
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Backref will create 'members' attribute on MemberStates
|
||||
# members = relationship('Members', back_populates='current_state')
|
||||
|
||||
class Members(Base):
|
||||
__tablename__ = 'members'
|
||||
|
||||
id = Column(Integer, primary_key=True) # Assuming 'id' is your primary key
|
||||
# ... other member fields ...
|
||||
|
||||
# Add foreign key constraint
|
||||
current_state_code = Column(String(1), ForeignKey('member_states.state_code'), index=True)
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
# Correct relationship definition
|
||||
current_state = relationship('MemberStates', back_populates='members')
|
||||
|
||||
# Update MemberStates to complete the bidirectional relationship
|
||||
MemberStates.members = relationship('Members', back_populates='current_state')
|
||||
|
||||
class MemberStateLog(Base):
|
||||
__tablename__ = 'member_state_log'
|
||||
|
||||
log_id = Column(Integer, primary_key=True)
|
||||
member_id = Column(Integer, ForeignKey('members.id'), nullable=False)
|
||||
state_code = Column(String(1), ForeignKey('member_states.state_code'), nullable=False)
|
||||
changed_at = Column(DateTime, default=datetime.utcnow)
|
||||
changed_by = Column(Integer) # Optional: user ID who made the change
|
||||
reason = Column(Text)
|
||||
|
||||
# Relationships
|
||||
member = relationship('Members', backref='state_history')
|
||||
state = relationship('MemberStates')
|
||||
|
||||
# OPTION 2: Alternative approach with primaryjoin (if you can't add foreign key)
|
||||
class MembersAlternative(Base):
|
||||
__tablename__ = 'members'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
current_state_code = Column(String(1), index=True) # No foreign key
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
# Use primaryjoin to specify the join condition
|
||||
current_state = relationship(
|
||||
'MemberStates',
|
||||
primaryjoin='Members.current_state_code == MemberStates.state_code',
|
||||
foreign_keys=[current_state_code]
|
||||
)
|
||||
|
||||
# USAGE EXAMPLES:
|
||||
|
||||
# Example 1: Query members with their current state
|
||||
def get_members_with_state():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
members = session.query(Members).join(Members.current_state).all()
|
||||
|
||||
for member in members:
|
||||
print(f"Member: {member.name}")
|
||||
print(f"State: {member.current_state.state_name}")
|
||||
print(f"Description: {member.current_state.description}")
|
||||
|
||||
session.close()
|
||||
|
||||
# Example 2: Get member state name directly
|
||||
def get_member_state_name(member):
|
||||
if member.current_state:
|
||||
return member.current_state.state_name
|
||||
return "Unknown"
|
||||
|
||||
# Example 3: Filter members by state
|
||||
def get_active_members():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
active_members = session.query(Members).filter(
|
||||
Members.current_state_code == 'A'
|
||||
).all()
|
||||
|
||||
session.close()
|
||||
return active_members
|
||||
|
||||
# Example 4: Count members by state
|
||||
def count_members_by_state():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import func
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
counts = session.query(
|
||||
MemberStates.state_name,
|
||||
func.count(Members.id).label('count')
|
||||
).outerjoin(Members).group_by(
|
||||
MemberStates.state_code,
|
||||
MemberStates.state_name
|
||||
).all()
|
||||
|
||||
session.close()
|
||||
return counts
|
||||
|
||||
# Example 5: Update member state (with logging)
|
||||
def change_member_state(session, member_id, new_state_code, changed_by=None, reason=None):
|
||||
# Get the member
|
||||
member = session.query(Members).get(member_id)
|
||||
if not member:
|
||||
raise ValueError(f"Member {member_id} not found")
|
||||
|
||||
# Validate state exists
|
||||
state = session.query(MemberStates).get(new_state_code)
|
||||
if not state:
|
||||
raise ValueError(f"Invalid state code: {new_state_code}")
|
||||
|
||||
# Log the state change
|
||||
log_entry = MemberStateLog(
|
||||
member_id=member_id,
|
||||
state_code=new_state_code,
|
||||
changed_by=changed_by,
|
||||
reason=reason
|
||||
)
|
||||
session.add(log_entry)
|
||||
|
||||
# Update member's current state
|
||||
member.current_state_code = new_state_code
|
||||
member.current_state_changed_at = datetime.utcnow()
|
||||
|
||||
session.commit()
|
||||
|
||||
# Example 6: Serialization for API responses
|
||||
def serialize_member_with_state(member):
|
||||
return {
|
||||
'id': member.id,
|
||||
'name': member.name, # Assuming you have a name field
|
||||
'current_state_code': member.current_state_code,
|
||||
'current_state_name': member.current_state.state_name if member.current_state else None,
|
||||
'current_state_description': member.current_state.description if member.current_state else None,
|
||||
'current_state_changed_at': member.current_state_changed_at.isoformat() if member.current_state_changed_at else None
|
||||
}
|
||||
|
||||
# Example 7: Query with eager loading to avoid N+1 queries
|
||||
def get_all_members_with_states_optimized():
|
||||
from sqlalchemy.orm import sessionmaker, joinedload
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
members = session.query(Members).options(
|
||||
joinedload(Members.current_state)
|
||||
).all()
|
||||
|
||||
# Now you can access member.current_state.state_name without additional queries
|
||||
result = [serialize_member_with_state(member) for member in members]
|
||||
|
||||
session.close()
|
||||
return result
|
||||
|
||||
# Example 8: Custom property for easier access
|
||||
class MembersWithProperty(Base):
|
||||
__tablename__ = 'members'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
current_state_code = Column(String(1), ForeignKey('member_states.state_code'), index=True)
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
current_state = relationship('MemberStates')
|
||||
|
||||
@property
|
||||
def state_name(self):
|
||||
return self.current_state.state_name if self.current_state else "Unknown"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.current_state_code == 'A'
|
||||
|
||||
@property
|
||||
def is_inactive(self):
|
||||
return self.current_state_code == 'I'
|
||||
|
||||
@property
|
||||
def is_deactivated(self):
|
||||
return self.current_state_code == 'D'
|
||||
Generated
+1061
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.0.6"
|
||||
}
|
||||
}
|
||||
+60
-2
@@ -1,4 +1,4 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from fastapi import FastAPI, HTTPException, Depends, WebSocket
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
@@ -7,6 +7,9 @@ from sustApp.models.models import Base, Members, Payments
|
||||
from sustApp.models.database import engine, SessionLocal
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
|
||||
#http://127.0.0.1:8000/docs -> swagger like ui
|
||||
#ngrok http http://localhost:8000
|
||||
@@ -27,6 +30,7 @@ origins = [
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:3000",
|
||||
"http://localhost:5173",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
@@ -37,6 +41,48 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Store active WebSocket connections
|
||||
connected_clients = []
|
||||
|
||||
# Custom logging handler to send logs via WebSocket
|
||||
class WebSocketHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
log_message = self.format(record)
|
||||
# Send to all connected clients
|
||||
asyncio.create_task(broadcast_log(log_message))
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger("app")
|
||||
logger.setLevel(logging.INFO)
|
||||
ws_handler = WebSocketHandler()
|
||||
logger.addHandler(ws_handler)
|
||||
|
||||
@app.websocket("/ws/logs")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
connected_clients.append(websocket)
|
||||
try:
|
||||
while True:
|
||||
await websocket.receive_text()
|
||||
except:
|
||||
connected_clients.remove(websocket)
|
||||
|
||||
async def broadcast_log(message):
|
||||
for client in connected_clients[:]: # Copy list to avoid modification during iteration
|
||||
try:
|
||||
await client.send_json({
|
||||
"type": "log",
|
||||
"message": message,
|
||||
"timestamp": asyncio.get_event_loop().time()
|
||||
})
|
||||
except:
|
||||
connected_clients.remove(client)
|
||||
|
||||
@app.get("/test")
|
||||
async def test_endpoint():
|
||||
logger.info("This is a test log message from backend!")
|
||||
return {"message": "Check your browser console"}
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: str | None = None
|
||||
@@ -69,6 +115,8 @@ class MemberBase(BaseModel):
|
||||
#payments: Optional[List[PaymentBase]]
|
||||
member_deliveries: Optional[List[DeliveryBase]]
|
||||
#deliveries: Mapped[List["DeliveryBase"]] = relationship()
|
||||
current_state_code: str | None = None
|
||||
current_state_name: str | None = None
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
@@ -86,11 +134,21 @@ async def read_members(db: db_dependency, skip: int = 0, limit: int = 100 ):
|
||||
raise HTTPException(status_code=400, detail='No members')
|
||||
return members
|
||||
|
||||
@app.get("/activememberscount/", response_model=int)
|
||||
async def read_activememberscount(db: db_dependency):
|
||||
count = db.query(Members).filter(Members.member_active == True).count()
|
||||
logger.info("This is a test log message from backend!")
|
||||
return count
|
||||
|
||||
@app.get("/member/{member_id}")
|
||||
async def read_member(member_id: int, db: db_dependency): #, token: Annotated[str, Depends(oauth2_scheme)]):
|
||||
result = db.query(Members).filter(Members.id == member_id).first()
|
||||
result = db.query(Members).join(Members.current_state).filter(Members.id == member_id).first()
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail='Member does not exist')
|
||||
print(f"Member: {result.member_surname}")
|
||||
print(f"State: {result.current_state.state_name}")
|
||||
print(f"Description: {result.current_state.description}")
|
||||
|
||||
return result
|
||||
|
||||
@app.get("/payments/{member_id}")
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, Float, Numeric
|
||||
from sqlalchemy.types import TypeDecorator, VARCHAR, Integer
|
||||
from sqlalchemy.types import TypeDecorator, VARCHAR, Integer, CHAR
|
||||
from sqlalchemy_utils import EmailType
|
||||
from sqlalchemy.orm import relationship
|
||||
from models.database import Base
|
||||
#from models.database import Base
|
||||
from sustApp.models.database import Base
|
||||
|
||||
import datetime
|
||||
|
||||
class TrimChar(TypeDecorator):
|
||||
impl = CHAR
|
||||
cache_key = False
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value:
|
||||
value = value.strip()
|
||||
return value
|
||||
|
||||
class NullCatchingString(TypeDecorator):
|
||||
impl = VARCHAR
|
||||
|
||||
@@ -50,6 +60,18 @@ class Members(Base):
|
||||
member_deliveries = relationship('Deliveries', backref='members')
|
||||
member_payments = relationship('Payments', backref='members')
|
||||
#member_deliveries: Mapped[List["Deliveries"]] = relationship()
|
||||
#current_state_code = Column(String, index=True)
|
||||
#current_state_name = relationship('MemberStates', backref='state_code')
|
||||
|
||||
current_state_code = Column(TrimChar, index=True) # No foreign key
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
# Use primaryjoin to specify the join condition
|
||||
current_state = relationship(
|
||||
'MemberStates',
|
||||
primaryjoin='Members.current_state_code == MemberStates.state_code',
|
||||
foreign_keys=[current_state_code]
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Comment "{self.content[:20]}...">'
|
||||
@@ -96,3 +118,25 @@ class Payments_CSV(Base):
|
||||
pc_currency = Column(String)
|
||||
pc_timestamp = Column(DateTime)
|
||||
pc_processed = Column(Boolean, default=False)
|
||||
|
||||
class MemberStates(Base):
|
||||
__tablename__ = 'member_states'
|
||||
|
||||
state_code = Column(TrimChar, primary_key=True, index=True)
|
||||
state_name = Column(String(20), nullable=False)
|
||||
description = Column(String)
|
||||
created_at = Column(DateTime)
|
||||
|
||||
class MemberStateLog(Base):
|
||||
__tablename__ = 'member_state_log'
|
||||
|
||||
log_id = Column(Integer, primary_key=True)
|
||||
member_id = Column(Integer, ForeignKey('members.id'), nullable=False)
|
||||
state_code = Column(String(1), ForeignKey('member_states.state_code'), nullable=False)
|
||||
changed_at = Column(DateTime)
|
||||
changed_by = Column(Integer) # Optional: user ID who made the change
|
||||
reason = Column(String)
|
||||
|
||||
# Relationships
|
||||
member = relationship('Members', backref='state_history')
|
||||
state = relationship('MemberStates')
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FastAPI Dashboard</title>
|
||||
<link href="./dist/output.css" rel="stylesheet" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1817
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "react-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^7.0.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Users,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Edit3,
|
||||
Trash2,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('ws://localhost:8000/ws/logs');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'log') {
|
||||
console.log(`[Backend Log]: ${data.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div>Your React App</div>;
|
||||
}
|
||||
|
||||
const Dashboard = () => {
|
||||
const [apiUrl, setApiUrl] = useState('http://127.0.0.1:8000');
|
||||
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [members, setMembers] = useState([]);
|
||||
const [totalMembers, setTotalMembers] = useState(0);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('Testing connection to:', apiUrl);
|
||||
// Test with a simple endpoint first - try /member/1 since /member/100 works
|
||||
const response = await fetch(`${apiUrl}/member/100`);
|
||||
|
||||
if (response.ok) {
|
||||
setConnectionStatus('connected');
|
||||
console.log('Connection successful!');
|
||||
// Load members data when connection is successful
|
||||
await loadMembersCount();
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
setConnectionStatus('error');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const loadMembersCount = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
console.log('Attempting to load active members count...');
|
||||
|
||||
// const fetchActiveMembersCount = async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/activememberscount`);
|
||||
if (response.ok) {
|
||||
const count = await response.json(); // Single number return
|
||||
setTotalMembers(count);
|
||||
console.log('Total active members count:', count);
|
||||
} else {
|
||||
console.log('Failed to fetch active members count:', response.statusText);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error fetching active members count:', error);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
//}
|
||||
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
try {
|
||||
// Since /members/ gives 500 error, let's try different approaches
|
||||
console.log('Attempting to load active members count...');
|
||||
|
||||
// Option 1: Try individual member requests (if you know IDs)
|
||||
// For now, let's try to get a few members by ID
|
||||
const memberIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Adjust these IDs as needed
|
||||
const memberPromises = memberIds.map(async (id) => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/member/${id}`);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log(`Member ${id} not found or error:`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(memberPromises);
|
||||
const validMembers = results.filter(member => member !== null);
|
||||
|
||||
console.log('Found members:', validMembers);
|
||||
setMembers(validMembers);
|
||||
setTotalMembers(validMembers.length);
|
||||
|
||||
if (validMembers.length === 0) {
|
||||
console.warn('No members found with IDs 1-10. You may need to adjust the ID range or fix the /members/ endpoint.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
// Load members when component mounts if already connected
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
loadMembersCount();
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return '#10b981';
|
||||
case 'error': return '#ef4444';
|
||||
default: return '#f59e0b';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return <CheckCircle size={16} />;
|
||||
case 'error': return <XCircle size={16} />;
|
||||
default: return <AlertCircle size={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'Connected';
|
||||
case 'error': return 'Connection Error';
|
||||
default: return 'Disconnected';
|
||||
}
|
||||
};
|
||||
|
||||
const getFullName = (member) => {
|
||||
const firstName = member.member_firstname || '';
|
||||
const lastName = member.member_surname || '';
|
||||
const memberSuffix = member.member_suffix || '';
|
||||
const fullName = `${firstName} ${lastName} ${memberSuffix}`.trim();
|
||||
return fullName || 'N/A';
|
||||
};
|
||||
|
||||
const MEMBER_STATE_COLORS = {
|
||||
'A': '#10B981', // Green - Active
|
||||
'I': '#F59E0B', // Amber/Orange - Inactive
|
||||
'D': '#EF4444' // Red - Deactivated
|
||||
};
|
||||
|
||||
const getMemberStateColor = (stateCode) => {
|
||||
return MEMBER_STATE_COLORS[stateCode] || '#6B7280'; // Gray as fallback
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f9fafb',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
},
|
||||
headerContent: {
|
||||
maxWidth: '1280px',
|
||||
margin: '0 auto',
|
||||
padding: '0 24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: '24px',
|
||||
paddingBottom: '24px'
|
||||
},
|
||||
title: {
|
||||
fontSize: '30px',
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
margin: 0
|
||||
},
|
||||
subtitle: {
|
||||
color: '#6b7280',
|
||||
marginTop: '4px',
|
||||
fontSize: '16px'
|
||||
},
|
||||
statusContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: getStatusColor(),
|
||||
fontWeight: '500'
|
||||
},
|
||||
mainContent: {
|
||||
maxWidth: '1280px',
|
||||
margin: '0 auto',
|
||||
padding: '32px 24px'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '24px',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
connectionForm: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px'
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none'
|
||||
},
|
||||
button: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#2563eb',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed'
|
||||
},
|
||||
errorAlert: {
|
||||
marginTop: '16px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
},
|
||||
errorText: {
|
||||
color: '#991b1b'
|
||||
},
|
||||
tabs: {
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
tabsNav: {
|
||||
display: 'flex',
|
||||
gap: '32px'
|
||||
},
|
||||
tab: {
|
||||
padding: '8px 4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid transparent',
|
||||
transition: 'all 0.2s'
|
||||
},
|
||||
tabActive: {
|
||||
borderBottom: '2px solid #2563eb',
|
||||
color: '#2563eb'
|
||||
},
|
||||
tabInactive: {
|
||||
color: '#6b7280'
|
||||
},
|
||||
statsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: '24px',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
statCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
statContent: {
|
||||
flex: 1
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#6b7280'
|
||||
},
|
||||
statValue: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
marginTop: '8px'
|
||||
},
|
||||
statIcon: {
|
||||
padding: '12px',
|
||||
borderRadius: '50%'
|
||||
},
|
||||
iconOrange: {
|
||||
backgroundColor: '#fed7aa',
|
||||
color: '#ea580c'
|
||||
},
|
||||
iconBlue: {
|
||||
backgroundColor: '#dbeafe',
|
||||
color: '#2563eb'
|
||||
},
|
||||
iconGreen: {
|
||||
backgroundColor: '#dcfce7',
|
||||
color: '#16a34a'
|
||||
},
|
||||
iconPurple: {
|
||||
backgroundColor: '#e9d5ff',
|
||||
color: '#9333ea'
|
||||
},
|
||||
actionButtons: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '16px'
|
||||
},
|
||||
actionButton: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
},
|
||||
actionButtonGray: {
|
||||
backgroundColor: '#f3f4f6',
|
||||
color: '#374151'
|
||||
},
|
||||
actionButtonGreen: {
|
||||
backgroundColor: '#dcfce7',
|
||||
color: '#166534'
|
||||
},
|
||||
actionButtonBlue: {
|
||||
backgroundColor: '#dbeafe',
|
||||
color: '#1e40af'
|
||||
},
|
||||
emptyState: {
|
||||
textAlign: 'center',
|
||||
padding: '32px'
|
||||
},
|
||||
emptyStateIcon: {
|
||||
margin: '0 auto 16px',
|
||||
color: '#9ca3af'
|
||||
},
|
||||
emptyStateText: {
|
||||
color: '#6b7280'
|
||||
},
|
||||
cardHeader: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px'
|
||||
},
|
||||
|
||||
refreshButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#3B82F6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
},
|
||||
|
||||
loadingState: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '40px',
|
||||
color: '#6B7280'
|
||||
},
|
||||
|
||||
loadingIcon: {
|
||||
marginBottom: '10px',
|
||||
color: '#3B82F6'
|
||||
},
|
||||
|
||||
dataContainer: {
|
||||
marginTop: '20px'
|
||||
},
|
||||
|
||||
dataHeader: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: '6px'
|
||||
},
|
||||
|
||||
dataCount: {
|
||||
margin: 0,
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
},
|
||||
|
||||
tableContainer: {
|
||||
overflowX: 'auto',
|
||||
border: '1px solid #E5E7EB',
|
||||
borderRadius: '8px'
|
||||
},
|
||||
|
||||
table: {
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse'
|
||||
},
|
||||
|
||||
tableHeaderRow: {
|
||||
backgroundColor: '#F9FAFB'
|
||||
},
|
||||
|
||||
tableHeader: {
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
borderBottom: '1px solid #E5E7EB'
|
||||
},
|
||||
|
||||
tableRow: {
|
||||
borderBottom: '1px solid #F3F4F6'
|
||||
},
|
||||
|
||||
tableCell: {
|
||||
padding: '12px',
|
||||
color: '#6B7280'
|
||||
},
|
||||
|
||||
statusBadge: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Header */}
|
||||
<div style={styles.header}>
|
||||
<div style={styles.headerContent}>
|
||||
<div>
|
||||
<h1 style={styles.title}>FastAPI Dashboard</h1>
|
||||
<p style={styles.subtitle}>Manage your backend data and monitor API status</p>
|
||||
</div>
|
||||
<div style={styles.statusContainer}>
|
||||
{getStatusIcon()}
|
||||
<span>{getStatusText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div style={styles.mainContent}>
|
||||
{/* Connection Section */}
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>API Connection</h2>
|
||||
<div style={styles.connectionForm}>
|
||||
<input
|
||||
type="text"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="Enter API URL"
|
||||
/>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
...styles.button,
|
||||
...(isLoading ? styles.buttonDisabled : {}),
|
||||
':hover': { backgroundColor: '#1d4ed8' }
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isLoading) e.target.style.backgroundColor = '#1d4ed8';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isLoading) e.target.style.backgroundColor = '#2563eb';
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} style={isLoading ? { animation: 'spin 1s linear infinite' } : {}} />
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
{connectionStatus === 'error' && (
|
||||
<div style={styles.errorAlert}>
|
||||
<XCircle size={20} style={{ color: '#dc2626' }} />
|
||||
<span style={styles.errorText}>NetworkError when attempting to fetch resource.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div style={styles.tabs}>
|
||||
<div style={styles.tabsNav}>
|
||||
<button
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'dashboard' ? styles.tabActive : styles.tabInactive)
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('data')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'data' ? styles.tabActive : styles.tabInactive)
|
||||
}}
|
||||
>
|
||||
Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div style={styles.statsGrid}>
|
||||
{/* API Status */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>API Status</p>
|
||||
<p style={styles.statValue}>
|
||||
{connectionStatus === 'connected' ? 'Online' : 'Offline'}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconOrange}}>
|
||||
<Server size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Members */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Active Members</p>
|
||||
<p style={styles.statValue}>{totalMembers}</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconBlue}}>
|
||||
<Database size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Users */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Active Users</p>
|
||||
<p style={styles.statValue}>1</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconGreen}}>
|
||||
<Users size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Activity</p>
|
||||
<p style={styles.statValue}>Live</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconPurple}}>
|
||||
<Activity size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>Quick Actions</h2>
|
||||
<div style={styles.actionButtons}>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonGray}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#e5e7eb'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#f3f4f6'}
|
||||
>
|
||||
<Eye size={16} />
|
||||
View Data
|
||||
</button>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonGreen}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#bbf7d0'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#dcfce7'}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Add Item
|
||||
</button>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonBlue}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#bfdbfe'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#dbeafe'}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'data' && (
|
||||
<div style={styles.card}>
|
||||
<div style={styles.cardHeader}>
|
||||
<h2 style={styles.cardTitle}>Data Management</h2>
|
||||
<button
|
||||
onClick={loadMembers}
|
||||
style={styles.refreshButton}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RefreshCw size={16} style={{ marginRight: '8px' }} />
|
||||
{isLoading ? 'Loading...' : 'Load Members'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={styles.loadingState}>
|
||||
<RefreshCw size={24} style={{ ...styles.loadingIcon, animation: 'spin 1s linear infinite' }} />
|
||||
<p>Loading members...</p>
|
||||
</div>
|
||||
) : members.length === 0 ? (
|
||||
<div style={styles.emptyState}>
|
||||
<Database size={48} style={styles.emptyStateIcon} />
|
||||
<p style={styles.emptyStateText}>No members found. Click "Load Members" to fetch data.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={styles.dataContainer}>
|
||||
<div style={styles.dataHeader}>
|
||||
<p style={styles.dataCount}>Total Members: {members.length}</p>
|
||||
</div>
|
||||
|
||||
<div style={styles.tableContainer}>
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr style={styles.tableHeaderRow}>
|
||||
<th style={styles.tableHeader}>ID</th>
|
||||
<th style={styles.tableHeader}>Name</th>
|
||||
<th style={styles.tableHeader}>Email</th>
|
||||
<th style={styles.tableHeader}>Active</th>
|
||||
<th style={styles.tableHeader}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{members.map((member, index) => (
|
||||
<tr key={member.id || index} style={styles.tableRow}>
|
||||
<td style={styles.tableCell}>{member.id}</td>
|
||||
<td style={styles.tableCell}>{getFullName(member)}</td>
|
||||
<td style={styles.tableCell}>{member.member_email || 'N/A'}</td>
|
||||
<td style={styles.tableCell}>
|
||||
<span style={{
|
||||
...styles.statusBadge,
|
||||
backgroundColor: getMemberStateColor(member.current_state.state_code),
|
||||
color: 'white'
|
||||
}}>
|
||||
{member.current_state.state_name}
|
||||
</span>
|
||||
</td>
|
||||
<td style={styles.tableCell}>
|
||||
<button style={styles.actionButton}>
|
||||
<Edit3 size={14} />
|
||||
</button>
|
||||
<button style={styles.actionButton}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,458 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, Plus, Edit3, Trash2, RefreshCw, AlertCircle, CheckCircle, Server, Database, Users, Activity } from 'lucide-react';
|
||||
|
||||
const FastAPIFrontend = () => {
|
||||
const [apiUrl, setApiUrl] = useState('http://localhost:8000');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [formData, setFormData] = useState({});
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
|
||||
// Test API connection
|
||||
const testConnection = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/health`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setIsConnected(false);
|
||||
setError(err.message || 'Connection failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch data from API
|
||||
const fetchData = async (endpoint = 'items') => {
|
||||
if (!isConnected) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/${endpoint}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setData(Array.isArray(result) ? result : [result]);
|
||||
} else {
|
||||
throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Create new item
|
||||
const createItem = async (itemData) => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/items`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(itemData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchData();
|
||||
setShowModal(false);
|
||||
setFormData({});
|
||||
} else {
|
||||
throw new Error('Failed to create item');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Update item
|
||||
const updateItem = async (id, itemData) => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/items/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(itemData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchData();
|
||||
setShowModal(false);
|
||||
setEditingItem(null);
|
||||
setFormData({});
|
||||
} else {
|
||||
throw new Error('Failed to update item');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete item
|
||||
const deleteItem = async (id) => {
|
||||
if (!window.confirm('Are you sure you want to delete this item?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/items/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchData();
|
||||
} else {
|
||||
throw new Error('Failed to delete item');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (editingItem) {
|
||||
updateItem(editingItem.id, formData);
|
||||
} else {
|
||||
createItem(formData);
|
||||
}
|
||||
};
|
||||
|
||||
// Open edit modal
|
||||
const startEdit = (item) => {
|
||||
setEditingItem(item);
|
||||
setFormData(item);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
testConnection();
|
||||
}, [apiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && activeTab === 'data') {
|
||||
fetchData();
|
||||
}
|
||||
}, [isConnected, activeTab]);
|
||||
|
||||
const ConnectionStatus = () => (
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
||||
<span className={`text-sm font-medium ${isConnected ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
<span className="text-gray-500">•</span>
|
||||
<span className="text-sm text-gray-600">{apiUrl}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Dashboard = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100">API Status</p>
|
||||
<p className="text-2xl font-bold">{isConnected ? 'Online' : 'Offline'}</p>
|
||||
</div>
|
||||
<Server className="w-8 h-8 text-blue-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-green-500 to-green-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-green-100">Total Items</p>
|
||||
<p className="text-2xl font-bold">{data.length}</p>
|
||||
</div>
|
||||
<Database className="w-8 h-8 text-green-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-purple-500 to-purple-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100">Active Users</p>
|
||||
<p className="text-2xl font-bold">1</p>
|
||||
</div>
|
||||
<Users className="w-8 h-8 text-purple-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-orange-500 to-orange-600 p-6 rounded-xl text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100">Activity</p>
|
||||
<p className="text-2xl font-bold">Live</p>
|
||||
</div>
|
||||
<Activity className="w-8 h-8 text-orange-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">Quick Actions</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('data')}
|
||||
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
<Database className="w-6 h-6 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-sm font-medium text-gray-600">View Data</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({});
|
||||
setEditingItem(null);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-green-300 hover:bg-green-50 transition-colors"
|
||||
>
|
||||
<Plus className="w-6 h-6 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-sm font-medium text-gray-600">Add Item</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={testConnection}
|
||||
className="p-4 border-2 border-dashed border-gray-200 rounded-lg hover:border-purple-300 hover:bg-purple-50 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-6 h-6 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-sm font-medium text-gray-600">Refresh</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DataView = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Data Management</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({});
|
||||
setEditingItem(null);
|
||||
setShowModal(true);
|
||||
}}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add New</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-blue-600" />
|
||||
<span className="ml-2 text-gray-600">Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="4" className="px-6 py-12 text-center text-gray-500">
|
||||
No data available. Add some items to get started.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((item, index) => (
|
||||
<tr key={item.id || index} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{item.id || index + 1}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{item.name || item.title || 'N/A'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600 max-w-xs truncate">
|
||||
{item.description || item.content || 'No description'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => startEdit(item)}
|
||||
className="text-blue-600 hover:text-blue-900 p-1 rounded"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteItem(item.id)}
|
||||
className="text-red-600 hover:text-red-900 p-1 rounded"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Modal = () => (
|
||||
showModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl max-w-md w-full p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
{editingItem ? 'Edit Item' : 'Add New Item'}
|
||||
</h3>
|
||||
|
||||
<div onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<textarea
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => setFormData({...formData, description: e.target.value})}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
rows="3"
|
||||
placeholder="Enter description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
setEditingItem(null);
|
||||
setFormData({});
|
||||
}}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
{editingItem ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">FastAPI Dashboard</h1>
|
||||
<p className="text-sm text-gray-600">Manage your backend data and monitor API status</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="API URL"
|
||||
/>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={loading}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span>Test</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'dashboard'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('data')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'data'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<ConnectionStatus />
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6 flex items-center space-x-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
<span className="text-red-700">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'dashboard' ? <Dashboard /> : <DataView />}
|
||||
</div>
|
||||
|
||||
<Modal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FastAPIFrontend;
|
||||
@@ -0,0 +1,233 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Users,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [apiUrl, setApiUrl] = useState('http://localhost:8000');
|
||||
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setConnectionStatus('connected');
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'text-green-600';
|
||||
case 'error': return 'text-red-600';
|
||||
default: return 'text-orange-600';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return <CheckCircle className="w-4 h-4" />;
|
||||
case 'error': return <XCircle className="w-4 h-4" />;
|
||||
default: return <AlertCircle className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'Connected';
|
||||
case 'error': return 'Connection Error';
|
||||
default: return 'Disconnected';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">FastAPI Dashboard</h1>
|
||||
<p className="text-gray-600 mt-1">Manage your backend data and monitor API status</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`flex items-center space-x-2 ${getStatusColor()}`}>
|
||||
{getStatusIcon()}
|
||||
<span className="font-medium">{getStatusText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection Section */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">API Connection</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter API URL"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={isLoading}
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
{connectionStatus === 'error' && (
|
||||
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<XCircle className="w-5 h-5 text-red-400 mr-2" />
|
||||
<span className="text-red-800">NetworkError when attempting to fetch resource.</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="mb-8">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'dashboard'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('data')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'data'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Data
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{/* API Status */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">API Status</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">
|
||||
{connectionStatus === 'connected' ? 'Online' : 'Offline'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-orange-100 p-3 rounded-full">
|
||||
<Server className="w-6 h-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Items */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Items</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">0</p>
|
||||
</div>
|
||||
<div className="bg-blue-100 p-3 rounded-full">
|
||||
<Database className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Users */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Active Users</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">1</p>
|
||||
</div>
|
||||
<div className="bg-green-100 p-3 rounded-full">
|
||||
<Users className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Activity</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">Live</p>
|
||||
</div>
|
||||
<div className="bg-purple-100 p-3 rounded-full">
|
||||
<Activity className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button className="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View Data
|
||||
</button>
|
||||
<button className="inline-flex items-center px-4 py-2 bg-green-100 text-green-700 rounded-md hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Item
|
||||
</button>
|
||||
<button className="inline-flex items-center px-4 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'data' && (
|
||||
<div className="mt-8 bg-white rounded-lg shadow-sm border p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Data Management</h2>
|
||||
<div className="text-center py-8">
|
||||
<Database className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">No data available. Connect to your API to view data.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,515 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Users,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [apiUrl, setApiUrl] = useState('http://127.0.0.1:8000');
|
||||
const [connectionStatus, setConnectionStatus] = useState('disconnected');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [members, setMembers] = useState([]);
|
||||
const [totalMembers, setTotalMembers] = useState(0);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log('Testing connection to:', apiUrl);
|
||||
const response = await fetch(`${apiUrl}/member/100`);
|
||||
|
||||
if (response.ok) {
|
||||
setConnectionStatus('connected');
|
||||
console.log('Connection successful!');
|
||||
// Load members data when connection is successful
|
||||
await loadMembers();
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
setConnectionStatus('error');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (connectionStatus !== 'connected' && !isLoading) return;
|
||||
|
||||
setLoadingData(true);
|
||||
try {
|
||||
console.log('Loading members from:', `${apiUrl}/members/?skip=0&limit=100`);
|
||||
const response = await fetch(`${apiUrl}/members/?skip=0&limit=100`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Members data:', data);
|
||||
|
||||
// Handle different response formats
|
||||
if (Array.isArray(data)) {
|
||||
setMembers(data);
|
||||
setTotalMembers(data.length);
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
// If your API returns {items: [...], total: 123} format
|
||||
setMembers(data.items);
|
||||
setTotalMembers(data.total || data.items.length);
|
||||
} else {
|
||||
console.warn('Unexpected data format:', data);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to load members: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
setMembers([]);
|
||||
setTotalMembers(0);
|
||||
}
|
||||
setLoadingData(false);
|
||||
};
|
||||
|
||||
// Load members when component mounts if already connected
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
loadMembers();
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return '#10b981';
|
||||
case 'error': return '#ef4444';
|
||||
default: return '#f59e0b';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return <CheckCircle size={16} />;
|
||||
case 'error': return <XCircle size={16} />;
|
||||
default: return <AlertCircle size={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected': return 'Connected';
|
||||
case 'error': return 'Connection Error';
|
||||
default: return 'Disconnected';
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f9fafb',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
},
|
||||
headerContent: {
|
||||
maxWidth: '1280px',
|
||||
margin: '0 auto',
|
||||
padding: '0 24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: '24px',
|
||||
paddingBottom: '24px'
|
||||
},
|
||||
title: {
|
||||
fontSize: '30px',
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
margin: 0
|
||||
},
|
||||
subtitle: {
|
||||
color: '#6b7280',
|
||||
marginTop: '4px',
|
||||
fontSize: '16px'
|
||||
},
|
||||
statusContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: getStatusColor(),
|
||||
fontWeight: '500'
|
||||
},
|
||||
mainContent: {
|
||||
maxWidth: '1280px',
|
||||
margin: '0 auto',
|
||||
padding: '32px 24px'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '24px',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
connectionForm: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px'
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
outline: 'none'
|
||||
},
|
||||
button: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#2563eb',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed'
|
||||
},
|
||||
errorAlert: {
|
||||
marginTop: '16px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#fef2f2',
|
||||
border: '1px solid #fecaca',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
},
|
||||
errorText: {
|
||||
color: '#991b1b'
|
||||
},
|
||||
tabs: {
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
tabsNav: {
|
||||
display: 'flex',
|
||||
gap: '32px'
|
||||
},
|
||||
tab: {
|
||||
padding: '8px 4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid transparent',
|
||||
transition: 'all 0.2s'
|
||||
},
|
||||
tabActive: {
|
||||
borderBottom: '2px solid #2563eb',
|
||||
color: '#2563eb'
|
||||
},
|
||||
tabInactive: {
|
||||
color: '#6b7280'
|
||||
},
|
||||
statsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: '24px',
|
||||
marginBottom: '32px'
|
||||
},
|
||||
statCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
statContent: {
|
||||
flex: 1
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#6b7280'
|
||||
},
|
||||
statValue: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
marginTop: '8px'
|
||||
},
|
||||
statIcon: {
|
||||
padding: '12px',
|
||||
borderRadius: '50%'
|
||||
},
|
||||
iconOrange: {
|
||||
backgroundColor: '#fed7aa',
|
||||
color: '#ea580c'
|
||||
},
|
||||
iconBlue: {
|
||||
backgroundColor: '#dbeafe',
|
||||
color: '#2563eb'
|
||||
},
|
||||
iconGreen: {
|
||||
backgroundColor: '#dcfce7',
|
||||
color: '#16a34a'
|
||||
},
|
||||
iconPurple: {
|
||||
backgroundColor: '#e9d5ff',
|
||||
color: '#9333ea'
|
||||
},
|
||||
actionButtons: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '16px'
|
||||
},
|
||||
actionButton: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
},
|
||||
actionButtonGray: {
|
||||
backgroundColor: '#f3f4f6',
|
||||
color: '#374151'
|
||||
},
|
||||
actionButtonGreen: {
|
||||
backgroundColor: '#dcfce7',
|
||||
color: '#166534'
|
||||
},
|
||||
actionButtonBlue: {
|
||||
backgroundColor: '#dbeafe',
|
||||
color: '#1e40af'
|
||||
},
|
||||
emptyState: {
|
||||
textAlign: 'center',
|
||||
padding: '32px'
|
||||
},
|
||||
emptyStateIcon: {
|
||||
margin: '0 auto 16px',
|
||||
color: '#9ca3af'
|
||||
},
|
||||
emptyStateText: {
|
||||
color: '#6b7280'
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Header */}
|
||||
<div style={styles.header}>
|
||||
<div style={styles.headerContent}>
|
||||
<div>
|
||||
<h1 style={styles.title}>FastAPI Dashboard</h1>
|
||||
<p style={styles.subtitle}>Manage your backend data and monitor API status</p>
|
||||
</div>
|
||||
<div style={styles.statusContainer}>
|
||||
{getStatusIcon()}
|
||||
<span>{getStatusText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div style={styles.mainContent}>
|
||||
{/* Connection Section */}
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>API Connection</h2>
|
||||
<div style={styles.connectionForm}>
|
||||
<input
|
||||
type="text"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
style={styles.input}
|
||||
placeholder="Enter API URL"
|
||||
/>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
...styles.button,
|
||||
...(isLoading ? styles.buttonDisabled : {}),
|
||||
':hover': { backgroundColor: '#1d4ed8' }
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isLoading) e.target.style.backgroundColor = '#1d4ed8';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isLoading) e.target.style.backgroundColor = '#2563eb';
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} style={isLoading ? { animation: 'spin 1s linear infinite' } : {}} />
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
{connectionStatus === 'error' && (
|
||||
<div style={styles.errorAlert}>
|
||||
<XCircle size={20} style={{ color: '#dc2626' }} />
|
||||
<span style={styles.errorText}>NetworkError when attempting to fetch resource.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div style={styles.tabs}>
|
||||
<div style={styles.tabsNav}>
|
||||
<button
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'dashboard' ? styles.tabActive : styles.tabInactive)
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('data')}
|
||||
style={{
|
||||
...styles.tab,
|
||||
...(activeTab === 'data' ? styles.tabActive : styles.tabInactive)
|
||||
}}
|
||||
>
|
||||
Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div style={styles.statsGrid}>
|
||||
{/* API Status */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>API Status</p>
|
||||
<p style={styles.statValue}>
|
||||
{connectionStatus === 'connected' ? 'Online' : 'Offline'}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconOrange}}>
|
||||
<Server size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Items */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Total Items</p>
|
||||
<p style={styles.statValue}>0</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconBlue}}>
|
||||
<Database size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Users */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Active Users</p>
|
||||
<p style={styles.statValue}>1</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconGreen}}>
|
||||
<Users size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity */}
|
||||
<div style={styles.statCard}>
|
||||
<div style={styles.statContent}>
|
||||
<p style={styles.statLabel}>Activity</p>
|
||||
<p style={styles.statValue}>Live</p>
|
||||
</div>
|
||||
<div style={{...styles.statIcon, ...styles.iconPurple}}>
|
||||
<Activity size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>Quick Actions</h2>
|
||||
<div style={styles.actionButtons}>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonGray}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#e5e7eb'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#f3f4f6'}
|
||||
>
|
||||
<Eye size={16} />
|
||||
View Data
|
||||
</button>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonGreen}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#bbf7d0'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#dcfce7'}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Add Item
|
||||
</button>
|
||||
<button
|
||||
style={{...styles.actionButton, ...styles.actionButtonBlue}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#bfdbfe'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#dbeafe'}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'data' && (
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.cardTitle}>Data Management</h2>
|
||||
<div style={styles.emptyState}>
|
||||
<Database size={48} style={styles.emptyStateIcon} />
|
||||
<p style={styles.emptyStateText}>No data available. Connect to your API to view data.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import FastAPIFrontend from './FastAPIFrontend';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<FastAPIFrontend />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user